dls-dodal 1.29.4__py3-none-any.whl → 1.30.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 (85) hide show
  1. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/METADATA +27 -42
  2. dls_dodal-1.30.0.dist-info/RECORD +132 -0
  3. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/WHEEL +1 -1
  4. dls_dodal-1.30.0.dist-info/entry_points.txt +3 -0
  5. dodal/__init__.py +1 -4
  6. dodal/_version.py +2 -2
  7. dodal/beamlines/__init__.py +3 -1
  8. dodal/beamlines/i03.py +28 -23
  9. dodal/beamlines/i04.py +34 -12
  10. dodal/beamlines/i13_1.py +66 -0
  11. dodal/beamlines/i22.py +5 -5
  12. dodal/beamlines/i24.py +1 -1
  13. dodal/beamlines/p38.py +7 -7
  14. dodal/beamlines/p45.py +7 -5
  15. dodal/beamlines/p99.py +61 -0
  16. dodal/cli.py +6 -3
  17. dodal/common/beamlines/beamline_parameters.py +2 -2
  18. dodal/common/beamlines/beamline_utils.py +6 -5
  19. dodal/common/maths.py +1 -3
  20. dodal/common/types.py +2 -3
  21. dodal/common/udc_directory_provider.py +14 -3
  22. dodal/common/visit.py +2 -3
  23. dodal/devices/CTAB.py +22 -17
  24. dodal/devices/aperturescatterguard.py +114 -136
  25. dodal/devices/areadetector/adaravis.py +8 -6
  26. dodal/devices/areadetector/adsim.py +2 -3
  27. dodal/devices/areadetector/adutils.py +20 -12
  28. dodal/devices/cryostream.py +19 -7
  29. dodal/devices/detector/__init__.py +13 -2
  30. dodal/devices/detector/det_dim_constants.py +2 -2
  31. dodal/devices/detector/det_dist_to_beam_converter.py +1 -1
  32. dodal/devices/detector/detector.py +5 -5
  33. dodal/devices/detector/detector_motion.py +38 -31
  34. dodal/devices/eiger.py +11 -15
  35. dodal/devices/eiger_odin.py +9 -10
  36. dodal/devices/fast_grid_scan.py +4 -3
  37. dodal/devices/fluorescence_detector_motion.py +13 -4
  38. dodal/devices/focusing_mirror.py +4 -4
  39. dodal/devices/hutch_shutter.py +4 -4
  40. dodal/devices/i22/dcm.py +4 -3
  41. dodal/devices/i22/fswitch.py +4 -4
  42. dodal/devices/i22/nxsas.py +23 -32
  43. dodal/devices/i24/pmac.py +47 -8
  44. dodal/devices/ipin.py +7 -4
  45. dodal/devices/linkam3.py +11 -5
  46. dodal/devices/logging_ophyd_device.py +1 -1
  47. dodal/devices/motors.py +31 -5
  48. dodal/devices/oav/grid_overlay.py +1 -0
  49. dodal/devices/oav/microns_for_zoom_levels.json +1 -1
  50. dodal/devices/oav/oav_detector.py +2 -1
  51. dodal/devices/oav/oav_parameters.py +18 -10
  52. dodal/devices/oav/oav_to_redis_forwarder.py +100 -0
  53. dodal/devices/oav/pin_image_recognition/__init__.py +6 -6
  54. dodal/devices/oav/pin_image_recognition/utils.py +5 -6
  55. dodal/devices/oav/utils.py +2 -2
  56. dodal/devices/p99/__init__.py +0 -0
  57. dodal/devices/p99/sample_stage.py +43 -0
  58. dodal/devices/robot.py +30 -18
  59. dodal/devices/scintillator.py +8 -5
  60. dodal/devices/smargon.py +3 -3
  61. dodal/devices/status.py +2 -31
  62. dodal/devices/tetramm.py +4 -4
  63. dodal/devices/thawer.py +5 -3
  64. dodal/devices/undulator_dcm.py +6 -8
  65. dodal/devices/util/adjuster_plans.py +2 -2
  66. dodal/devices/util/epics_util.py +5 -7
  67. dodal/devices/util/lookup_tables.py +2 -3
  68. dodal/devices/util/save_panda.py +87 -0
  69. dodal/devices/util/test_utils.py +17 -0
  70. dodal/devices/webcam.py +3 -3
  71. dodal/devices/xbpm_feedback.py +0 -23
  72. dodal/devices/zebra.py +10 -10
  73. dodal/devices/zebra_controlled_shutter.py +3 -3
  74. dodal/devices/zocalo/zocalo_interaction.py +10 -2
  75. dodal/devices/zocalo/zocalo_results.py +31 -18
  76. dodal/log.py +14 -5
  77. dodal/plans/data_session_metadata.py +1 -0
  78. dodal/plans/motor_util_plans.py +117 -0
  79. dodal/utils.py +65 -22
  80. dls_dodal-1.29.4.dist-info/RECORD +0 -125
  81. dls_dodal-1.29.4.dist-info/entry_points.txt +0 -2
  82. dodal/devices/qbpm1.py +0 -8
  83. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/LICENSE +0 -0
  84. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/top_level.txt +0 -0
  85. /dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +0 -0
@@ -1,37 +1,44 @@
1
- from enum import IntEnum
1
+ from enum import Enum
2
2
 
3
- from ophyd import Component as Cpt
4
- from ophyd import Device, EpicsMotor, EpicsSignal, EpicsSignalRO
3
+ from ophyd_async.core import Device
4
+ from ophyd_async.epics.motion import Motor
5
+ from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
5
6
 
6
7
 
7
- class ShutterState(IntEnum):
8
- CLOSED = 0
9
- OPEN = 1
8
+ class ShutterState(str, Enum):
9
+ CLOSED = "Closed"
10
+ OPEN = "Open"
10
11
 
11
12
 
12
13
  class DetectorMotion(Device):
13
- """Physical motion and interlocks for detector travel"""
14
-
15
- upstream_x = Cpt(EpicsMotor, "-MO-DET-01:UPSTREAMX")
16
- downstream_x = Cpt(EpicsMotor, "-MO-DET-01:DOWNSTREAMX")
17
- x = Cpt(EpicsMotor, "-MO-DET-01:X")
18
- y = Cpt(EpicsMotor, "-MO-DET-01:Y")
19
- z = Cpt(EpicsMotor, "-MO-DET-01:Z")
20
- yaw = Cpt(EpicsMotor, "-MO-DET-01:YAW")
21
- shutter = Cpt(EpicsSignal, "-MO-DET-01:SET_SHUTTER_STATE") # 0=closed, 1=open
22
- # monitors
23
- shutter_closed_lim = Cpt(
24
- EpicsSignalRO, "-MO-DET-01:CLOSE_LIMIT"
25
- ) # on limit = 1, off = 0
26
- shutter_open_lim = Cpt(
27
- EpicsSignalRO, "-MO-DET-01:OPEN_LIMIT"
28
- ) # on limit = 1, off = 0
29
- z_disabled = Cpt(
30
- EpicsSignalRO, "-MO-DET-01:Z:DISABLED"
31
- ) # robot interlock, 0=ok to move, 1=blocked
32
- crate_power = Cpt(
33
- EpicsSignalRO, "-MO-PMAC-02:CRATE2_HEALTHY"
34
- ) # returns 0 if no power
35
- in_robot_load_safe_position = Cpt(
36
- EpicsSignalRO, "-MO-PMAC-02:GPIO_INP_BITS.B2"
37
- ) # returns 1 if safe
14
+ def __init__(self, prefix: str, name: str = ""):
15
+ device_prefix = "-MO-DET-01:"
16
+ pmac_prefix = "-MO-PMAC-02:"
17
+
18
+ self.upstream_x = Motor(f"{prefix}{device_prefix}UPSTREAMX")
19
+ self.downstream_x = Motor(f"{prefix}{device_prefix}DOWNSTREAMX")
20
+ self.x = Motor(f"{prefix}{device_prefix}X")
21
+ self.y = Motor(f"{prefix}{device_prefix}Y")
22
+ self.z = Motor(f"{prefix}{device_prefix}Z")
23
+ self.yaw = Motor(f"{prefix}{device_prefix}YAW")
24
+
25
+ self.shutter = epics_signal_rw(
26
+ ShutterState, f"{prefix}{device_prefix}SET_SHUTTER_STATE"
27
+ )
28
+ self.shutter_closed_lim = epics_signal_r(
29
+ float, f"{prefix}{device_prefix}CLOSE_LIMIT"
30
+ ) # on limit = 1, off = 0
31
+ self.shutter_open_lim = epics_signal_r(
32
+ float, f"{prefix}{device_prefix}OPEN_LIMIT"
33
+ ) # on limit = 1, off = 0
34
+ self.z_disabled = epics_signal_r(
35
+ float, f"{prefix}{device_prefix}Z:DISABLED"
36
+ ) # robot interlock, 0=ok to move, 1=blocked
37
+ self.crate_power = epics_signal_r(
38
+ float, f"{prefix}{pmac_prefix}CRATE2_HEALTHY"
39
+ ) # returns 0 if no power
40
+ self.in_robot_load_safe_position = epics_signal_r(
41
+ int, f"{prefix}{pmac_prefix}GPIO_INP_BITS.B2"
42
+ ) # returns 1 if safe
43
+
44
+ super().__init__(name)
dodal/devices/eiger.py CHANGED
@@ -1,3 +1,4 @@
1
+ # type: ignore # Eiger will soon be ophyd-async https://github.com/DiamondLightSource/dodal/issues/700
1
2
  from enum import Enum
2
3
 
3
4
  from ophyd import Component, Device, EpicsSignalRO, Signal
@@ -12,10 +13,6 @@ from dodal.log import LOGGER
12
13
 
13
14
  FREE_RUN_MAX_IMAGES = 1000000
14
15
 
15
- # TODO present for testing purposes, remove
16
- TEST_1169_FIX = True
17
- TEST_1169_INJECT = False
18
-
19
16
 
20
17
  class InternalEigerTriggerMode(Enum):
21
18
  INTERNAL_SERIES = 0
@@ -56,17 +53,15 @@ class EigerDetector(Device):
56
53
  cls,
57
54
  params: DetectorParams,
58
55
  name: str = "EigerDetector",
59
- *args,
60
- **kwargs,
61
56
  ):
62
- det = cls(name=name, *args, **kwargs)
57
+ det = cls(name=name)
63
58
  det.set_detector_parameters(params)
64
59
  return det
65
60
 
66
61
  def set_detector_parameters(self, detector_params: DetectorParams):
67
62
  self.detector_params = detector_params
68
63
  if self.detector_params is None:
69
- raise Exception("Parameters for scan must be specified")
64
+ raise ValueError("Parameters for scan must be specified")
70
65
 
71
66
  to_check = [
72
67
  (
@@ -103,7 +98,7 @@ class EigerDetector(Device):
103
98
 
104
99
  def stage(self):
105
100
  self.wait_on_arming_if_started()
106
- if TEST_1169_INJECT or not self.is_armed():
101
+ if not self.is_armed():
107
102
  LOGGER.info("Eiger not armed, arming")
108
103
 
109
104
  self.async_stage().wait(timeout=self.ARMING_TIMEOUT)
@@ -318,7 +313,9 @@ class EigerDetector(Device):
318
313
 
319
314
  def _finish_arm(self) -> Status:
320
315
  LOGGER.info("Eiger staging: Finishing arming")
321
- return Status(done=True, success=True)
316
+ status = Status()
317
+ status.set_finished()
318
+ return status
322
319
 
323
320
  def forward_bit_depth_to_filewriter(self):
324
321
  bit_depth = self.bit_depth.get()
@@ -341,6 +338,10 @@ class EigerDetector(Device):
341
338
  functions_to_do_arm.append(self.enable_roi_mode)
342
339
 
343
340
  arming_sequence_funcs = [
341
+ # If a beam dump occurs after arming the eiger but prior to eiger staging,
342
+ # the odin may timeout which will cause the arming sequence to be retried;
343
+ # if this previously completed successfully we must reset the odin first
344
+ self.odin.stop,
344
345
  lambda: self.change_dev_shm(detector_params.enable_dev_shm),
345
346
  lambda: self.set_detector_threshold(detector_params.expected_energy_ev),
346
347
  self.set_cam_pvs,
@@ -354,11 +355,6 @@ class EigerDetector(Device):
354
355
  self._wait_fan_ready,
355
356
  self._finish_arm,
356
357
  ]
357
- if TEST_1169_FIX:
358
- # If a beam dump occurs after arming the eiger but prior to eiger staging,
359
- # the odin may timeout which will cause the arming sequence to be retried;
360
- # if this previously completed successfully we must reset the odin first
361
- arming_sequence_funcs.insert(0, self.odin.stop)
362
358
 
363
359
  functions_to_do_arm.extend(arming_sequence_funcs)
364
360
 
@@ -1,5 +1,4 @@
1
- from typing import List, Tuple
2
-
1
+ # type: ignore # Eiger will soon be ophyd-async https://github.com/DiamondLightSource/dodal/issues/700
3
2
  from ophyd import Component, Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
4
3
  from ophyd.areadetector.plugins import HDF5Plugin_V22
5
4
  from ophyd.sim import NullStatus
@@ -59,12 +58,12 @@ class OdinNodesStatus(Device):
59
58
  node_3 = Component(OdinNode, "OD4:")
60
59
 
61
60
  @property
62
- def nodes(self) -> List[OdinNode]:
61
+ def nodes(self) -> list[OdinNode]:
63
62
  return [self.node_0, self.node_1, self.node_2, self.node_3]
64
63
 
65
64
  def check_node_frames_from_attr(
66
65
  self, node_get_func, error_message_verb: str
67
- ) -> Tuple[bool, str]:
66
+ ) -> tuple[bool, str]:
68
67
  nodes_frames_values = [0] * len(self.nodes)
69
68
  frames_details = []
70
69
  for node_number, node_pv in enumerate(self.nodes):
@@ -75,17 +74,17 @@ class OdinNodesStatus(Device):
75
74
  bad_frames = any(v != 0 for v in nodes_frames_values)
76
75
  return bad_frames, "\n".join(frames_details)
77
76
 
78
- def check_frames_timed_out(self) -> Tuple[bool, str]:
77
+ def check_frames_timed_out(self) -> tuple[bool, str]:
79
78
  return self.check_node_frames_from_attr(
80
79
  lambda node: node.frames_timed_out.get(), "timed out"
81
80
  )
82
81
 
83
- def check_frames_dropped(self) -> Tuple[bool, str]:
82
+ def check_frames_dropped(self) -> tuple[bool, str]:
84
83
  return self.check_node_frames_from_attr(
85
84
  lambda node: node.frames_dropped.get(), "dropped"
86
85
  )
87
86
 
88
- def get_error_state(self) -> Tuple[bool, str]:
87
+ def get_error_state(self) -> tuple[bool, str]:
89
88
  is_error = []
90
89
  error_messages = []
91
90
  for node_number, node_pv in enumerate(self.nodes):
@@ -99,7 +98,7 @@ class OdinNodesStatus(Device):
99
98
 
100
99
  def get_init_state(self) -> bool:
101
100
  is_initialised = []
102
- for node_number, node_pv in enumerate(self.nodes):
101
+ for node_pv in self.nodes:
103
102
  is_initialised.append(node_pv.fr_initialised.get())
104
103
  is_initialised.append(node_pv.fp_initialised.get())
105
104
  return all(is_initialised)
@@ -132,7 +131,7 @@ class EigerOdin(Device):
132
131
  frames_timed_out, frames_timed_out_details = self.nodes.check_frames_timed_out()
133
132
 
134
133
  if not is_initialised:
135
- raise Exception(error_message)
134
+ raise RuntimeError(error_message)
136
135
  if frames_dropped:
137
136
  self.log.error(f"Frames dropped: {frames_dropped_details}")
138
137
  if frames_timed_out:
@@ -140,7 +139,7 @@ class EigerOdin(Device):
140
139
 
141
140
  return is_initialised and not frames_dropped and not frames_timed_out
142
141
 
143
- def check_odin_initialised(self) -> Tuple[bool, str]:
142
+ def check_odin_initialised(self) -> tuple[bool, str]:
144
143
  is_error_state, error_messages = self.nodes.get_error_state()
145
144
  to_check = [
146
145
  (not self.fan.consumers_connected.get(), "EigerFan is not connected"),
@@ -126,7 +126,7 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
126
126
  :return: The motor position this corresponds to.
127
127
  :raises: IndexError if the desired position is outside the grid."""
128
128
  for position, axis in zip(
129
- grid_position, [self.x_axis, self.y_axis, self.z_axis]
129
+ grid_position, [self.x_axis, self.y_axis, self.z_axis], strict=False
130
130
  ):
131
131
  if not axis.is_within(position):
132
132
  raise IndexError(f"{grid_position} is outside the bounds of the grid")
@@ -191,9 +191,10 @@ class MotionProgram(Device):
191
191
  class ExpectedImages(SignalR[int]):
192
192
  def __init__(self, parent: "FastGridScanCommon") -> None:
193
193
  super().__init__(SoftSignalBackend(int))
194
- self.parent: "FastGridScanCommon" = parent
194
+ self.parent = parent
195
195
 
196
- async def get_value(self):
196
+ async def get_value(self, cached: bool | None = None):
197
+ assert isinstance(self.parent, FastGridScanCommon)
197
198
  x = await self.parent.x_steps.get_value()
198
199
  y = await self.parent.y_steps.get_value()
199
200
  z = await self.parent.z_steps.get_value()
@@ -1,9 +1,18 @@
1
- from ophyd import Component as Cpt
2
- from ophyd import Device, EpicsSignal
1
+ from enum import Enum
3
2
 
3
+ from ophyd_async.core import StandardReadable
4
+ from ophyd_async.epics.signal import epics_signal_r
4
5
 
5
- class FluorescenceDetector(Device):
6
+
7
+ class FluorescenceDetectorControlState(Enum):
6
8
  OUT = 0
7
9
  IN = 1
8
10
 
9
- pos = Cpt(EpicsSignal, "-EA-FLU-01:CTRL")
11
+
12
+ class FluorescenceDetector(StandardReadable):
13
+ def __init__(self, prefix: str, name: str = ""):
14
+ with self.add_children_as_readables():
15
+ self.pos = epics_signal_r(
16
+ FluorescenceDetectorControlState, prefix + "-EA-FLU-01:CTRL"
17
+ )
18
+ super().__init__(name)
@@ -2,8 +2,10 @@ from enum import Enum
2
2
 
3
3
  from ophyd_async.core import (
4
4
  AsyncStatus,
5
+ ConfigSignal,
5
6
  Device,
6
7
  DeviceVector,
8
+ HintedSignal,
7
9
  StandardReadable,
8
10
  observe_value,
9
11
  )
@@ -144,10 +146,8 @@ class FocusingMirror(StandardReadable):
144
146
  # regardless of orientation of the mirror
145
147
  self.incident_angle = Motor(prefix + "PITCH")
146
148
 
147
- self.set_readable_signals(
148
- read=[self.incident_angle.user_readback],
149
- config=[self.type],
150
- )
149
+ self.add_readables([self.incident_angle.user_readback], wrapper=HintedSignal)
150
+ self.add_readables([self.type], wrapper=ConfigSignal)
151
151
  super().__init__(name)
152
152
 
153
153
 
@@ -74,20 +74,20 @@ class HutchShutter(StandardReadable, Movable):
74
74
  super().__init__(name)
75
75
 
76
76
  @AsyncStatus.wrap
77
- async def set(self, position_demand: ShutterDemand):
77
+ async def set(self, value: ShutterDemand):
78
78
  interlock_state = await self.interlock.shutter_safe_to_operate()
79
79
  if not interlock_state:
80
80
  raise ShutterNotSafeToOperateError(
81
81
  "The hutch has not been locked, not operating shutter."
82
82
  )
83
- if position_demand == ShutterDemand.OPEN:
83
+ if value == ShutterDemand.OPEN:
84
84
  await self.control.set(ShutterDemand.RESET, wait=True)
85
- await self.control.set(position_demand, wait=True)
85
+ await self.control.set(value, wait=True)
86
86
  return await wait_for_value(
87
87
  self.status, match=ShutterState.OPEN, timeout=DEFAULT_TIMEOUT
88
88
  )
89
89
  else:
90
- await self.control.set(position_demand, wait=True)
90
+ await self.control.set(value, wait=True)
91
91
  return await wait_for_value(
92
92
  self.status, match=ShutterState.CLOSED, timeout=DEFAULT_TIMEOUT
93
93
  )
dodal/devices/i22/dcm.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import time
2
+ from collections.abc import Sequence
2
3
  from dataclasses import dataclass
3
- from typing import Dict, Literal, Sequence
4
+ from typing import Literal
4
5
 
5
6
  from bluesky.protocols import Reading
6
7
  from event_model.documents.event_descriptor import DataKey
@@ -127,7 +128,7 @@ class DoubleCrystalMonochromator(StandardReadable):
127
128
 
128
129
  super().__init__(name)
129
130
 
130
- async def describe(self) -> Dict[str, DataKey]:
131
+ async def describe(self) -> dict[str, DataKey]:
131
132
  default_describe = await super().describe()
132
133
  return {
133
134
  f"{self.name}-wavelength": DataKey(
@@ -139,7 +140,7 @@ class DoubleCrystalMonochromator(StandardReadable):
139
140
  **default_describe,
140
141
  }
141
142
 
142
- async def read(self) -> Dict[str, Reading]:
143
+ async def read(self) -> dict[str, Reading]:
143
144
  default_reading = await super().read()
144
145
  energy: float = default_reading[f"{self.name}-energy"]["value"]
145
146
  if energy > 0.0:
@@ -1,9 +1,9 @@
1
1
  import asyncio
2
2
  import time
3
3
  from enum import Enum
4
- from typing import Dict
5
4
 
6
- from bluesky.protocols import DataKey, Reading
5
+ from bluesky.protocols import Reading
6
+ from event_model import DataKey
7
7
  from ophyd_async.core import ConfigSignal, StandardReadable, soft_signal_r_and_setter
8
8
  from ophyd_async.core.device import DeviceVector
9
9
  from ophyd_async.epics.signal import epics_signal_r
@@ -74,7 +74,7 @@ class FSwitch(StandardReadable):
74
74
 
75
75
  super().__init__(name)
76
76
 
77
- async def describe(self) -> Dict[str, DataKey]:
77
+ async def describe(self) -> dict[str, DataKey]:
78
78
  default_describe = await super().describe()
79
79
  return {
80
80
  FSwitch.NUM_LENSES_FIELD_NAME: DataKey(
@@ -83,7 +83,7 @@ class FSwitch(StandardReadable):
83
83
  **default_describe,
84
84
  }
85
85
 
86
- async def read(self) -> Dict[str, Reading]:
86
+ async def read(self) -> dict[str, Reading]:
87
87
  result = await asyncio.gather(
88
88
  *(filter.get_value() for filter in self.filters.values())
89
89
  )
@@ -1,5 +1,4 @@
1
1
  from dataclasses import dataclass, fields
2
- from typing import Dict
3
2
 
4
3
  from bluesky.protocols import Reading
5
4
  from event_model.documents.event_descriptor import DataKey
@@ -13,7 +12,7 @@ ValueAndUnits = tuple[float, str]
13
12
  @dataclass
14
13
  class MetadataHolder:
15
14
  # TODO: just in case this is useful more widely...
16
- async def describe(self, parent_name: str) -> Dict[str, DataKey]:
15
+ async def describe(self, parent_name: str) -> dict[str, DataKey]:
17
16
  def datakey(value) -> DataKey:
18
17
  if isinstance(value, tuple):
19
18
  return {"units": value[1], **datakey(value[0])}
@@ -40,8 +39,8 @@ class MetadataHolder:
40
39
  if getattr(self, field.name, None) is not None
41
40
  }
42
41
 
43
- async def read(self, parent_name: str) -> Dict[str, Reading]:
44
- def reading(value):
42
+ async def read(self, parent_name: str) -> dict[str, Reading]:
43
+ def reading(value) -> Reading:
45
44
  if isinstance(value, tuple):
46
45
  return reading(value[0])
47
46
  return {"timestamp": -1, "value": value}
@@ -101,25 +100,21 @@ class NXSasPilatus(PilatusDetector):
101
100
  )
102
101
  self._metadata_holder = metadata_holder
103
102
 
104
- async def read_configuration(self) -> Dict[str, Reading]:
103
+ async def read_configuration(self) -> dict[str, Reading]:
105
104
  return await merge_gathered_dicts(
106
- (
107
- r
108
- for r in (
109
- super().read_configuration(),
110
- self._metadata_holder.read(self.name),
111
- )
105
+ r
106
+ for r in (
107
+ super().read_configuration(),
108
+ self._metadata_holder.read(self.name),
112
109
  )
113
110
  )
114
111
 
115
- async def describe_configuration(self) -> Dict[str, DataKey]:
112
+ async def describe_configuration(self) -> dict[str, DataKey]:
116
113
  return await merge_gathered_dicts(
117
- (
118
- r
119
- for r in (
120
- super().describe_configuration(),
121
- self._metadata_holder.describe(self.name),
122
- )
114
+ r
115
+ for r in (
116
+ super().describe_configuration(),
117
+ self._metadata_holder.describe(self.name),
123
118
  )
124
119
  )
125
120
 
@@ -150,24 +145,20 @@ class NXSasOAV(AravisDetector):
150
145
  )
151
146
  self._metadata_holder = metadata_holder
152
147
 
153
- async def read_configuration(self) -> Dict[str, Reading]:
148
+ async def read_configuration(self) -> dict[str, Reading]:
154
149
  return await merge_gathered_dicts(
155
- (
156
- r
157
- for r in (
158
- super().read_configuration(),
159
- self._metadata_holder.read(self.name),
160
- )
150
+ r
151
+ for r in (
152
+ super().read_configuration(),
153
+ self._metadata_holder.read(self.name),
161
154
  )
162
155
  )
163
156
 
164
- async def describe_configuration(self) -> Dict[str, DataKey]:
157
+ async def describe_configuration(self) -> dict[str, DataKey]:
165
158
  return await merge_gathered_dicts(
166
- (
167
- r
168
- for r in (
169
- super().describe_configuration(),
170
- self._metadata_holder.describe(self.name),
171
- )
159
+ r
160
+ for r in (
161
+ super().describe_configuration(),
162
+ self._metadata_holder.describe(self.name),
172
163
  )
173
164
  )
dodal/devices/i24/pmac.py CHANGED
@@ -1,8 +1,9 @@
1
- from enum import Enum
1
+ from enum import Enum, IntEnum
2
+ from typing import SupportsFloat
2
3
 
3
4
  from bluesky.protocols import Triggerable
4
- from ophyd_async.core import AsyncStatus, StandardReadable
5
- from ophyd_async.core.signal import SignalRW
5
+ from ophyd_async.core import AsyncStatus, StandardReadable, wait_for_value
6
+ from ophyd_async.core.signal import CalculateTimeout, SignalR, SignalRW
6
7
  from ophyd_async.core.signal_backend import SignalBackend
7
8
  from ophyd_async.core.soft_signal_backend import SoftSignalBackend
8
9
  from ophyd_async.core.utils import DEFAULT_TIMEOUT
@@ -13,6 +14,11 @@ HOME_STR = r"\#1hmz\#2hmz\#3hmz" # Command to home the PMAC motors
13
14
  ZERO_STR = "!x0y0z0" # Command to blend any ongoing move into new position
14
15
 
15
16
 
17
+ class ScanState(IntEnum):
18
+ RUNNING = 1
19
+ DONE = 0
20
+
21
+
16
22
  class LaserSettings(str, Enum):
17
23
  """PMAC strings to switch laser on and off.
18
24
  Note. On the PMAC, M-variables usually have to do with position compare
@@ -73,26 +79,55 @@ class PMACStringLaser(SignalRW):
73
79
  super().__init__(backend, timeout, name)
74
80
 
75
81
  @AsyncStatus.wrap
76
- async def set(self, laser_setting: LaserSettings):
77
- await self.signal.set(laser_setting.value, wait=True)
82
+ async def set(self, value: LaserSettings, wait=True, timeout=CalculateTimeout):
83
+ await self.signal.set(value.value, wait, timeout)
78
84
 
79
85
 
80
86
  class PMACStringEncReset(SignalRW):
81
- """"""
87
+ """Set a pmac_string to control the encoder channels in the controller."""
88
+
89
+ def __init__(
90
+ self,
91
+ pmac_str_sig: SignalRW,
92
+ backend: SignalBackend,
93
+ timeout: float | None = DEFAULT_TIMEOUT,
94
+ name: str = "",
95
+ ) -> None:
96
+ self.signal = pmac_str_sig
97
+ super().__init__(backend, timeout, name)
98
+
99
+ @AsyncStatus.wrap
100
+ async def set(self, value: EncReset, wait=True, timeout=CalculateTimeout):
101
+ await self.signal.set(value.value, wait, timeout)
102
+
103
+
104
+ class ProgramRunner(SignalRW):
105
+ """Trigger the collection by setting the program number on the PMAC string.
106
+
107
+ Once the program number has been set, wait for the collection to be complete.
108
+ This will only be true when the status becomes 0.
109
+ """
82
110
 
83
111
  def __init__(
84
112
  self,
85
113
  pmac_str_sig: SignalRW,
114
+ status_sig: SignalR,
86
115
  backend: SignalBackend,
87
116
  timeout: float | None = DEFAULT_TIMEOUT,
88
117
  name: str = "",
89
118
  ) -> None:
90
119
  self.signal = pmac_str_sig
120
+ self.status = status_sig
91
121
  super().__init__(backend, timeout, name)
92
122
 
93
123
  @AsyncStatus.wrap
94
- async def set(self, enc_string: EncReset):
95
- await self.signal.set(enc_string.value, wait=True)
124
+ async def set(self, value: int, wait=True, timeout=None):
125
+ prog_str = f"&2b{value}r"
126
+ assert isinstance(timeout, SupportsFloat) or (
127
+ timeout is None
128
+ ), f"ProgramRunner does not support calculating timeout itself, {timeout=}"
129
+ await self.signal.set(prog_str, wait=wait)
130
+ await wait_for_value(self.status, ScanState.DONE, timeout)
96
131
 
97
132
 
98
133
  class PMAC(StandardReadable):
@@ -121,4 +156,8 @@ class PMAC(StandardReadable):
121
156
  self.scanstatus = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2401")
122
157
  self.counter = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2402")
123
158
 
159
+ self.run_program = ProgramRunner(
160
+ self.pmac_string, self.scanstatus, backend=SoftSignalBackend(str)
161
+ )
162
+
124
163
  super().__init__(name)
dodal/devices/ipin.py CHANGED
@@ -1,8 +1,11 @@
1
- from ophyd import Component as Cpt
2
- from ophyd import Device, EpicsSignalRO, Kind
1
+ from ophyd_async.core import HintedSignal, StandardReadable
2
+ from ophyd_async.epics.signal import epics_signal_r
3
3
 
4
4
 
5
- class IPin(Device):
5
+ class IPin(StandardReadable):
6
6
  """Simple device to get the ipin reading"""
7
7
 
8
- reading = Cpt(EpicsSignalRO, "I", kind=Kind.hinted)
8
+ def __init__(self, prefix: str, name: str = "") -> None:
9
+ with self.add_children_as_readables(wrapper=HintedSignal):
10
+ self.pin_readback = epics_signal_r(float, prefix + "I")
11
+ super().__init__(name)
dodal/devices/linkam3.py CHANGED
@@ -1,10 +1,15 @@
1
1
  import asyncio
2
2
  import time
3
3
  from enum import Enum
4
- from typing import Optional
5
4
 
6
5
  from bluesky.protocols import Location
7
- from ophyd_async.core import StandardReadable, WatchableAsyncStatus, observe_value
6
+ from ophyd_async.core import (
7
+ ConfigSignal,
8
+ HintedSignal,
9
+ StandardReadable,
10
+ WatchableAsyncStatus,
11
+ observe_value,
12
+ )
8
13
  from ophyd_async.core.utils import WatcherUpdate
9
14
  from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
10
15
 
@@ -57,14 +62,15 @@ class Linkam3(StandardReadable):
57
62
  # status is a bitfield stored in a double?
58
63
  self.status = epics_signal_r(float, prefix + "STATUS:")
59
64
 
60
- self.set_readable_signals(
61
- read=(self.temp,), config=(self.ramp_rate, self.speed, self.set_point)
65
+ self.add_readables((self.temp,), wrapper=HintedSignal)
66
+ self.add_readables(
67
+ (self.ramp_rate, self.speed, self.set_point), wrapper=ConfigSignal
62
68
  )
63
69
 
64
70
  super().__init__(name=name)
65
71
 
66
72
  @WatchableAsyncStatus.wrap
67
- async def set(self, new_position: float, timeout: Optional[float] = None):
73
+ async def set(self, new_position: float, timeout: float | None = None):
68
74
  # time.monotonic won't go backwards in case of NTP corrections
69
75
  start = time.monotonic()
70
76
  old_position = await self.set_point.get_value()
@@ -3,7 +3,7 @@ from ophyd.log import logger as ophyd_logger
3
3
 
4
4
 
5
5
  class InfoLoggingDevice(Device):
6
- def wait_for_connection(self, all_signals=False, timeout=2):
6
+ def wait_for_connection(self, all_signals=False, timeout=2.0):
7
7
  class_name = self.__class__.__name__
8
8
  ophyd_logger.info(
9
9
  f"{class_name} waiting for connection, {'not' if all_signals else ''} waiting for all signals, timeout = {timeout}s.",