ophyd-async 0.12.2__py3-none-any.whl → 0.13.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 (31) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +11 -0
  3. ophyd_async/core/_detector.py +6 -9
  4. ophyd_async/core/_enums.py +21 -0
  5. ophyd_async/epics/adaravis/__init__.py +1 -2
  6. ophyd_async/epics/adaravis/_aravis_controller.py +4 -4
  7. ophyd_async/epics/adaravis/_aravis_io.py +2 -12
  8. ophyd_async/epics/adcore/__init__.py +4 -4
  9. ophyd_async/epics/adcore/_core_io.py +59 -7
  10. ophyd_async/epics/adcore/_core_logic.py +4 -3
  11. ophyd_async/epics/adcore/_core_writer.py +4 -5
  12. ophyd_async/epics/adcore/_hdf_writer.py +6 -6
  13. ophyd_async/epics/adcore/_utils.py +36 -11
  14. ophyd_async/epics/advimba/__init__.py +0 -2
  15. ophyd_async/epics/advimba/_vimba_controller.py +6 -9
  16. ophyd_async/epics/advimba/_vimba_io.py +3 -10
  17. ophyd_async/epics/core/__init__.py +2 -0
  18. ophyd_async/epics/core/_aioca.py +6 -2
  19. ophyd_async/epics/core/_util.py +12 -0
  20. ophyd_async/epics/eiger/_odin_io.py +25 -7
  21. ophyd_async/epics/pmac/__init__.py +7 -2
  22. ophyd_async/epics/pmac/_pmac_io.py +34 -23
  23. ophyd_async/epics/pmac/_utils.py +231 -0
  24. ophyd_async/fastcs/eiger/_eiger.py +1 -1
  25. ophyd_async/fastcs/eiger/_eiger_io.py +2 -1
  26. ophyd_async/plan_stubs/_nd_attributes.py +11 -37
  27. {ophyd_async-0.12.2.dist-info → ophyd_async-0.13.0.dist-info}/METADATA +3 -2
  28. {ophyd_async-0.12.2.dist-info → ophyd_async-0.13.0.dist-info}/RECORD +31 -29
  29. {ophyd_async-0.12.2.dist-info → ophyd_async-0.13.0.dist-info}/WHEEL +0 -0
  30. {ophyd_async-0.12.2.dist-info → ophyd_async-0.13.0.dist-info}/licenses/LICENSE +0 -0
  31. {ophyd_async-0.12.2.dist-info → ophyd_async-0.13.0.dist-info}/top_level.txt +0 -0
@@ -6,6 +6,7 @@ from event_model import DataKey # type: ignore
6
6
 
7
7
  from ophyd_async.core import (
8
8
  DEFAULT_TIMEOUT,
9
+ AsyncStatus,
9
10
  DetectorWriter,
10
11
  Device,
11
12
  DeviceVector,
@@ -21,6 +22,7 @@ from ophyd_async.epics.core import (
21
22
  epics_signal_r,
22
23
  epics_signal_rw,
23
24
  epics_signal_rw_rbv,
25
+ stop_busy_record,
24
26
  )
25
27
 
26
28
 
@@ -68,10 +70,15 @@ class Odin(Device):
68
70
 
69
71
  self.file_path = epics_signal_rw_rbv(str, f"{prefix}FilePath")
70
72
  self.file_name = epics_signal_rw_rbv(str, f"{prefix}FileName")
73
+ self.id = epics_signal_r(str, f"{prefix}AcquisitionID_RBV")
71
74
 
72
75
  self.num_frames_chunks = epics_signal_rw(int, prefix + "NumFramesChunks")
73
76
  self.meta_active = epics_signal_r(str, prefix + "META:AcquisitionActive_RBV")
74
77
  self.meta_writing = epics_signal_r(str, prefix + "META:Writing_RBV")
78
+ self.meta_file_name = epics_signal_r(str, f"{prefix}META:FileName_RBV")
79
+ self.meta_stop = epics_signal_rw(bool, f"{prefix}META:Stop")
80
+
81
+ self.fan_ready = epics_signal_rw(float, f"{prefix}FAN:StateReady_RBV")
75
82
 
76
83
  self.data_type = epics_signal_rw_rbv(str, f"{prefix}DataType")
77
84
 
@@ -88,6 +95,7 @@ class OdinWriter(DetectorWriter):
88
95
  self._drv = odin_driver
89
96
  self._path_provider = path_provider
90
97
  self._eiger_bit_depth = Reference(eiger_bit_depth)
98
+ self._capture_status: AsyncStatus | None = None
91
99
  super().__init__()
92
100
 
93
101
  async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]:
@@ -95,17 +103,23 @@ class OdinWriter(DetectorWriter):
95
103
  self._exposures_per_event = exposures_per_event
96
104
 
97
105
  await asyncio.gather(
98
- self._drv.file_path.set(str(info.directory_path)),
99
- self._drv.file_name.set(info.filename),
100
106
  self._drv.data_type.set(f"UInt{await self._eiger_bit_depth().get_value()}"),
101
107
  self._drv.num_to_capture.set(0),
108
+ self._drv.file_path.set(str(info.directory_path)),
109
+ self._drv.file_name.set(info.filename),
102
110
  )
103
111
 
104
- await wait_for_value(self._drv.meta_active, "Active", timeout=DEFAULT_TIMEOUT)
112
+ await asyncio.gather(
113
+ wait_for_value(
114
+ self._drv.meta_file_name, info.filename, timeout=DEFAULT_TIMEOUT
115
+ ),
116
+ wait_for_value(self._drv.id, info.filename, timeout=DEFAULT_TIMEOUT),
117
+ wait_for_value(self._drv.meta_active, "Active", timeout=DEFAULT_TIMEOUT),
118
+ )
105
119
 
106
- await self._drv.capture.set(
107
- Writing.CAPTURE, wait=False
108
- ) # TODO: Investigate why we do not get a put callback when setting capture pv https://github.com/bluesky/ophyd-async/issues/866
120
+ self._capture_status = await set_and_wait_for_value(
121
+ self._drv.capture, Writing.CAPTURE, wait_for_set_completion=False
122
+ )
109
123
 
110
124
  await asyncio.gather(
111
125
  wait_for_value(self._drv.capture_rbv, "Capturing", timeout=DEFAULT_TIMEOUT),
@@ -146,4 +160,8 @@ class OdinWriter(DetectorWriter):
146
160
  raise NotImplementedError()
147
161
 
148
162
  async def close(self) -> None:
149
- await set_and_wait_for_value(self._drv.capture, Writing.DONE)
163
+ await stop_busy_record(self._drv.capture, Writing.DONE, timeout=DEFAULT_TIMEOUT)
164
+ await self._drv.meta_stop.set(True, wait=True)
165
+ if self._capture_status and not self._capture_status.done:
166
+ await self._capture_status
167
+ self._capture_status = None
@@ -1,3 +1,8 @@
1
- from ._pmac_io import PmacAxisIO, PmacCoordIO, PmacIO, PmacTrajectoryIO
1
+ from ._pmac_io import PmacAxisAssignmentIO, PmacCoordIO, PmacIO, PmacTrajectoryIO
2
2
 
3
- __all__ = ["PmacAxisIO", "PmacCoordIO", "PmacIO", "PmacTrajectoryIO"]
3
+ __all__ = [
4
+ "PmacAxisAssignmentIO",
5
+ "PmacCoordIO",
6
+ "PmacIO",
7
+ "PmacTrajectoryIO",
8
+ ]
@@ -3,6 +3,7 @@ from collections.abc import Sequence
3
3
  import numpy as np
4
4
 
5
5
  from ophyd_async.core import Array1D, Device, DeviceVector, StandardReadable
6
+ from ophyd_async.epics import motor
6
7
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
7
8
 
8
9
  CS_LETTERS = "ABCUVWXYZ"
@@ -13,52 +14,53 @@ class PmacTrajectoryIO(StandardReadable):
13
14
 
14
15
  def __init__(self, prefix: str, name: str = "") -> None:
15
16
  self.time_array = epics_signal_rw(
16
- Array1D[np.float64], prefix + ":ProfileTimeArray"
17
+ Array1D[np.float64], prefix + "ProfileTimeArray"
17
18
  )
18
- self.user_array = epics_signal_rw(Array1D[np.int32], prefix + ":UserArray")
19
+ self.user_array = epics_signal_rw(Array1D[np.int32], prefix + "UserArray")
19
20
  # 1 indexed CS axes so we can index into them from the compound motor input link
20
21
  self.positions = DeviceVector(
21
22
  {
22
23
  i + 1: epics_signal_rw(
23
- Array1D[np.float64], f"{prefix}:{letter}:Positions"
24
+ Array1D[np.float64], f"{prefix}{letter}:Positions"
24
25
  )
25
26
  for i, letter in enumerate(CS_LETTERS)
26
27
  }
27
28
  )
28
29
  self.use_axis = DeviceVector(
29
30
  {
30
- i + 1: epics_signal_rw(bool, f"{prefix}:{letter}:UseAxis")
31
+ i + 1: epics_signal_rw(bool, f"{prefix}{letter}:UseAxis")
31
32
  for i, letter in enumerate(CS_LETTERS)
32
33
  }
33
34
  )
34
35
  self.velocities = DeviceVector(
35
36
  {
36
37
  i + 1: epics_signal_rw(
37
- Array1D[np.float64], f"{prefix}:{letter}:Velocities"
38
+ Array1D[np.float64], f"{prefix}{letter}:Velocities"
38
39
  )
39
40
  for i, letter in enumerate(CS_LETTERS)
40
41
  }
41
42
  )
42
- self.points_to_build = epics_signal_rw(int, prefix + ":ProfilePointsToBuild")
43
- self.build_profile = epics_signal_rw(bool, prefix + ":ProfileBuild")
44
- self.execute_profile = epics_signal_rw(bool, prefix + ":ProfileExecute")
45
- self.scan_percent = epics_signal_r(float, prefix + ":TscanPercent_RBV")
46
- self.abort_profile = epics_signal_rw(bool, prefix + ":ProfileAbort")
47
- self.profile_cs_name = epics_signal_rw(str, prefix + ":ProfileCsName")
48
- self.calculate_velocities = epics_signal_rw(bool, prefix + ":ProfileCalcVel")
43
+ self.points_to_build = epics_signal_rw(int, prefix + "ProfilePointsToBuild")
44
+ self.build_profile = epics_signal_rw(bool, prefix + "ProfileBuild")
45
+ self.execute_profile = epics_signal_rw(bool, prefix + "ProfileExecute")
46
+ self.scan_percent = epics_signal_r(float, prefix + "TscanPercent_RBV")
47
+ self.abort_profile = epics_signal_rw(bool, prefix + "ProfileAbort")
48
+ self.profile_cs_name = epics_signal_rw(str, prefix + "ProfileCsName")
49
+ self.calculate_velocities = epics_signal_rw(bool, prefix + "ProfileCalcVel")
49
50
 
50
51
  super().__init__(name=name)
51
52
 
52
53
 
53
- class PmacAxisIO(Device):
54
+ class PmacAxisAssignmentIO(Device):
54
55
  """A Device that (direct) moves a PMAC Coordinate System Motor.
55
56
 
56
57
  Note that this does not go through a motor record.
57
58
  """
58
59
 
59
60
  def __init__(self, prefix: str, name: str = "") -> None:
60
- self.cs_axis_letter = epics_signal_r(str, f"{prefix}:CsAxis_RBV")
61
- self.cs_port = epics_signal_r(str, f"{prefix}:CsPort_RBV")
61
+ self.cs_axis_letter = epics_signal_r(str, f"{prefix}CsAxis_RBV")
62
+ self.cs_port = epics_signal_r(str, f"{prefix}CsPort_RBV")
63
+ self.cs_number = epics_signal_r(int, f"{prefix}CsRaw_RBV")
62
64
  super().__init__(name=name)
63
65
 
64
66
 
@@ -66,12 +68,11 @@ class PmacCoordIO(Device):
66
68
  """A Device that represents a Pmac Coordinate System."""
67
69
 
68
70
  def __init__(self, prefix: str, name: str = "") -> None:
69
- self.defer_moves = epics_signal_r(bool, f"{prefix}:DeferMoves")
71
+ self.defer_moves = epics_signal_rw(bool, f"{prefix}DeferMoves")
72
+ self.cs_port = epics_signal_r(str, f"{prefix}Port")
70
73
  self.cs_axis_setpoint = DeviceVector(
71
74
  {
72
- i + 1: epics_signal_rw(
73
- Array1D[np.float64], f"{prefix}:M{i + 1}:DirectDemand"
74
- )
75
+ i + 1: epics_signal_rw(np.float64, f"{prefix}M{i + 1}:DirectDemand")
75
76
  for i in range(len(CS_LETTERS))
76
77
  }
77
78
  )
@@ -84,15 +85,25 @@ class PmacIO(Device):
84
85
  def __init__(
85
86
  self,
86
87
  prefix: str,
87
- axis_nums: Sequence[int],
88
+ raw_motors: Sequence[motor.Motor],
88
89
  coord_nums: Sequence[int],
89
90
  name: str = "",
90
91
  ) -> None:
91
- self.axis = DeviceVector(
92
- {axis: PmacAxisIO(f"{prefix}:M{axis}") for axis in axis_nums}
92
+ motor_prefixes = [motor.motor_egu.source.split(".")[0] for motor in raw_motors]
93
+
94
+ self.assignment = DeviceVector(
95
+ {
96
+ i: PmacAxisAssignmentIO(motor_prefix)
97
+ for i, motor_prefix in enumerate(motor_prefixes)
98
+ }
93
99
  )
100
+
101
+ # Public Look up for motor to axis assignment DeviceVector index
102
+
103
+ self.motor_assignment_index = {motor: i for i, motor in enumerate(raw_motors)}
104
+
94
105
  self.coord = DeviceVector(
95
- {coord: PmacCoordIO(f"{prefix}:CS{coord}") for coord in coord_nums}
106
+ {coord: PmacCoordIO(prefix=f"{prefix}CS{coord}:") for coord in coord_nums}
96
107
  )
97
108
  # Trajectory PVs have the same prefix as the pmac device
98
109
  self.trajectory = PmacTrajectoryIO(prefix)
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Sequence
5
+ from dataclasses import dataclass
6
+
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+ from scanspec.core import Slice
10
+
11
+ from ophyd_async.core import error_if_none, gather_dict
12
+ from ophyd_async.epics.motor import Motor
13
+
14
+ from ._pmac_io import CS_LETTERS, PmacIO
15
+
16
+ # PMAC durations are in milliseconds
17
+ # We must convert from scanspec durations (seconds) to milliseconds
18
+ # PMAC motion program multiples durations by 0.001
19
+ # (see https://github.com/DiamondLightSource/pmac/blob/afe81f8bb9179c3a20eff351f30bc6cfd1539ad9/pmacApp/pmc/trajectory_scan_code_ppmac.pmc#L241)
20
+ # Therefore, we must divide scanspec durations by 10e-6
21
+ TICK_S = 0.000001
22
+
23
+
24
+ @dataclass
25
+ class _Trajectory:
26
+ positions: dict[Motor, np.ndarray]
27
+ velocities: dict[Motor, np.ndarray]
28
+ user_programs: npt.NDArray[np.int32]
29
+ durations: npt.NDArray[np.float64]
30
+
31
+ @classmethod
32
+ def from_slice(cls, slice: Slice[Motor], ramp_up_time: float) -> _Trajectory:
33
+ """Parse a trajectory with no gaps from a slice.
34
+
35
+ :param slice: Information about a series of scan frames along a number of axes
36
+ :param ramp_up_duration: Time required to ramp up to speed
37
+ :param ramp_down: Booleon representing if we ramp down or not
38
+ :returns Trajectory: Data class representing our parsed trajectory
39
+ :raises RuntimeError: Slice must have no gaps and a duration array
40
+ """
41
+ slice_duration = error_if_none(slice.duration, "Slice must have a duration")
42
+
43
+ # Check if any gaps other than initial gap.
44
+ if any(slice.gap[1:]):
45
+ raise RuntimeError(
46
+ f"Cannot parse trajectory with gaps. Slice has gaps: {slice.gap}"
47
+ )
48
+
49
+ scan_size = len(slice)
50
+ motors = slice.axes()
51
+
52
+ positions: dict[Motor, npt.NDArray[np.float64]] = {}
53
+ velocities: dict[Motor, npt.NDArray[np.float64]] = {}
54
+
55
+ # Initialise arrays
56
+ positions = {motor: np.empty(2 * scan_size + 1, float) for motor in motors}
57
+ velocities = {motor: np.empty(2 * scan_size + 1, float) for motor in motors}
58
+ durations: npt.NDArray[np.float64] = np.empty(2 * scan_size + 1, float)
59
+ user_programs: npt.NDArray[np.int32] = np.ones(2 * scan_size + 1, float)
60
+ user_programs[-1] = 8
61
+
62
+ # Ramp up time for start of collection window
63
+ durations[0] = int(ramp_up_time / TICK_S)
64
+ # Half the time per point
65
+ durations[1:] = np.repeat(slice_duration / (2 * TICK_S), 2)
66
+
67
+ # Fill profile assuming no gaps
68
+ # Excluding starting points, we begin at our next frame
69
+ half_durations = slice_duration / 2
70
+ for motor in motors:
71
+ # Set the first position to be lower bound, then
72
+ # alternate mid and upper as the upper and lower
73
+ # bounds of neighbouring points are the same as gap is false
74
+ positions[motor][0] = slice.lower[motor][0]
75
+ positions[motor][1::2] = slice.midpoints[motor]
76
+ positions[motor][2::2] = slice.upper[motor]
77
+ # For velocities we will need the relative distances
78
+ mid_to_upper_velocities = (
79
+ slice.upper[motor] - slice.midpoints[motor]
80
+ ) / half_durations
81
+ lower_to_mid_velocities = (
82
+ slice.midpoints[motor] - slice.lower[motor]
83
+ ) / half_durations
84
+ # First velocity is the lower -> mid of first point
85
+ velocities[motor][0] = lower_to_mid_velocities[0]
86
+ # For the midpoints, we take the average of the
87
+ # lower -> mid and mid-> upper velocities of the same point
88
+ velocities[motor][1::2] = (
89
+ lower_to_mid_velocities + mid_to_upper_velocities
90
+ ) / 2
91
+ # For the upper points, we need to take the average of the
92
+ # mid -> upper velocity of the previous point and
93
+ # lower -> mid velocity of the current point
94
+ velocities[motor][2:-1:2] = (
95
+ mid_to_upper_velocities[:-1] + lower_to_mid_velocities[1:]
96
+ ) / 2
97
+ # For the last velocity take the mid to upper velocity
98
+ velocities[motor][-1] = mid_to_upper_velocities[-1]
99
+
100
+ return cls(
101
+ positions=positions,
102
+ velocities=velocities,
103
+ user_programs=user_programs,
104
+ durations=durations,
105
+ )
106
+
107
+
108
+ @dataclass
109
+ class _PmacMotorInfo:
110
+ cs_port: str
111
+ cs_number: int
112
+ motor_cs_index: dict[Motor, int]
113
+ motor_acceleration_rate: dict[Motor, float]
114
+
115
+ @classmethod
116
+ async def from_motors(cls, pmac: PmacIO, motors: Sequence[Motor]) -> _PmacMotorInfo:
117
+ """Creates a _PmacMotorInfo instance based on a controller and list of motors.
118
+
119
+ :param pmac: The PMAC device
120
+ :param motors: Sequence of motors involved in trajectory
121
+ :raises RuntimeError:
122
+ if motors do not share common CS port or CS number, or if
123
+ motors do not have unique CS index assignments
124
+ :returns:
125
+ _PmacMotorInfo instance with motor's common CS port and CS number, and
126
+ dictionaries of motor's to their unique CS index and accelerate rate
127
+
128
+ """
129
+ assignments = {
130
+ motor: pmac.assignment[pmac.motor_assignment_index[motor]]
131
+ for motor in motors
132
+ }
133
+
134
+ cs_ports, cs_numbers, cs_axes, velocities, accls = await asyncio.gather(
135
+ gather_dict(
136
+ {motor: assignments[motor].cs_port.get_value() for motor in motors}
137
+ ),
138
+ gather_dict(
139
+ {motor: assignments[motor].cs_number.get_value() for motor in motors}
140
+ ),
141
+ gather_dict(
142
+ {
143
+ motor: assignments[motor].cs_axis_letter.get_value()
144
+ for motor in motors
145
+ }
146
+ ),
147
+ gather_dict({motor: motor.max_velocity.get_value() for motor in motors}),
148
+ gather_dict(
149
+ {motor: motor.acceleration_time.get_value() for motor in motors}
150
+ ),
151
+ )
152
+
153
+ # check if the values in cs_port and cs_number are the same
154
+ cs_ports = set(cs_ports.values())
155
+
156
+ if len(cs_ports) != 1:
157
+ raise RuntimeError(
158
+ "Failed to fetch common CS port."
159
+ "Motors passed are assigned to multiple CS ports:"
160
+ f"{list(cs_ports)}"
161
+ )
162
+
163
+ cs_port = cs_ports.pop()
164
+
165
+ cs_numbers = set(cs_numbers.values())
166
+ if len(cs_numbers) != 1:
167
+ raise RuntimeError(
168
+ "Failed to fetch common CS number."
169
+ "Motors passed are assigned to multiple CS numbers:"
170
+ f"{list(cs_numbers)}"
171
+ )
172
+
173
+ cs_number = cs_numbers.pop()
174
+
175
+ motor_cs_index = {}
176
+ for motor in cs_axes:
177
+ try:
178
+ if not cs_axes[motor]:
179
+ raise ValueError
180
+ motor_cs_index[motor] = CS_LETTERS.index(cs_axes[motor])
181
+ except ValueError as err:
182
+ raise ValueError(
183
+ "Failed to get motor CS index. "
184
+ f"Motor {motor.name} assigned to '{cs_axes[motor]}' "
185
+ f"but must be assignmed to '{CS_LETTERS}"
186
+ ) from err
187
+ if len(set(motor_cs_index.values())) != len(motor_cs_index.items()):
188
+ raise RuntimeError(
189
+ "Failed to fetch distinct CS Axes."
190
+ "Motors passed are assigned to the same CS Axis"
191
+ f"{list(motor_cs_index)}"
192
+ )
193
+
194
+ motor_acceleration_rate = {
195
+ motor: float(velocities[motor]) / float(accls[motor])
196
+ for motor in velocities
197
+ }
198
+
199
+ return _PmacMotorInfo(
200
+ cs_port, cs_number, motor_cs_index, motor_acceleration_rate
201
+ )
202
+
203
+
204
+ def calculate_ramp_position_and_duration(
205
+ slice: Slice[Motor], motor_info: _PmacMotorInfo, is_up: bool
206
+ ) -> tuple[dict[Motor, float], float]:
207
+ if slice.duration is None:
208
+ raise ValueError("Slice must have a duration")
209
+
210
+ scan_axes = slice.axes()
211
+ idx = 0 if is_up else -1
212
+
213
+ velocities: dict[Motor, float] = {}
214
+ ramp_times: list[float] = []
215
+ for axis in scan_axes:
216
+ velocity = (slice.upper[axis][idx] - slice.lower[axis][idx]) / slice.duration[
217
+ idx
218
+ ]
219
+ velocities[axis] = velocity
220
+ ramp_times.append(abs(velocity) / motor_info.motor_acceleration_rate[axis])
221
+
222
+ max_ramp_time = max(ramp_times)
223
+
224
+ motor_to_ramp_position = {}
225
+ sign = -1 if is_up else 1
226
+ for axis, v in velocities.items():
227
+ ref_pos = slice.lower[axis][0] if is_up else slice.upper[axis][-1]
228
+ displacement = 0.5 * v * max_ramp_time
229
+ motor_to_ramp_position[axis] = ref_pos + sign * displacement
230
+
231
+ return (motor_to_ramp_position, max_ramp_time)
@@ -33,7 +33,7 @@ class EigerDetector(StandardDetector):
33
33
  OdinWriter(
34
34
  path_provider,
35
35
  self.odin,
36
- self.drv.detector.bit_depth_readout,
36
+ self.drv.detector.bit_depth_image,
37
37
  ),
38
38
  name=name,
39
39
  )
@@ -23,11 +23,12 @@ class EigerStreamIO(Device):
23
23
 
24
24
 
25
25
  class EigerDetectorIO(Device):
26
- bit_depth_readout: SignalR[int]
26
+ bit_depth_image: SignalR[int]
27
27
  state: SignalR[str]
28
28
  count_time: SignalRW[float]
29
29
  frame_time: SignalRW[float]
30
30
  nimages: SignalRW[int]
31
+ ntrigger: SignalRW[int]
31
32
  nexpi: SignalRW[int]
32
33
  trigger_mode: SignalRW[str]
33
34
  roi_mode: SignalRW[str]
@@ -1,57 +1,31 @@
1
1
  from collections.abc import Sequence
2
- from xml.etree import ElementTree as ET
3
2
 
4
3
  import bluesky.plan_stubs as bps
5
4
 
6
- from ophyd_async.core import Device
7
5
  from ophyd_async.epics.adcore import (
6
+ AreaDetector,
8
7
  NDArrayBaseIO,
9
8
  NDAttributeDataType,
10
9
  NDAttributeParam,
11
10
  NDAttributePv,
12
11
  NDFileHDFIO,
12
+ ndattributes_to_xml,
13
13
  )
14
14
 
15
15
 
16
16
  def setup_ndattributes(
17
- device: NDArrayBaseIO, ndattributes: Sequence[NDAttributePv | NDAttributeParam]
17
+ device: NDArrayBaseIO, ndattributes: Sequence[NDAttributeParam | NDAttributePv]
18
18
  ):
19
- """Set up attributes on NdArray devices."""
20
- root = ET.Element("Attributes")
21
-
22
- for ndattribute in ndattributes:
23
- if isinstance(ndattribute, NDAttributeParam):
24
- ET.SubElement(
25
- root,
26
- "Attribute",
27
- name=ndattribute.name,
28
- type="PARAM",
29
- source=ndattribute.param,
30
- addr=str(ndattribute.addr),
31
- datatype=ndattribute.datatype.value,
32
- description=ndattribute.description,
33
- )
34
- elif isinstance(ndattribute, NDAttributePv):
35
- ET.SubElement(
36
- root,
37
- "Attribute",
38
- name=ndattribute.name,
39
- type="EPICS_PV",
40
- source=ndattribute.signal.source.split("ca://")[-1],
41
- dbrtype=ndattribute.dbrtype.value,
42
- description=ndattribute.description,
43
- )
44
- else:
45
- raise ValueError(
46
- f"Invalid type for ndattributes: {type(ndattribute)}. "
47
- "Expected NDAttributePv or NDAttributeParam."
48
- )
49
- xml_text = ET.tostring(root, encoding="unicode")
50
- yield from bps.abs_set(device.nd_attributes_file, xml_text, wait=True)
19
+ xml = ndattributes_to_xml(ndattributes)
20
+ yield from bps.abs_set(
21
+ device.nd_attributes_file,
22
+ xml,
23
+ wait=True,
24
+ )
51
25
 
52
26
 
53
- def setup_ndstats_sum(detector: Device):
54
- """Set up nd stats for a detector."""
27
+ def setup_ndstats_sum(detector: AreaDetector):
28
+ """Set up nd stats sum nd attribute for a detector."""
55
29
  hdf = getattr(detector, "fileio", None)
56
30
  if not isinstance(hdf, NDFileHDFIO):
57
31
  msg = (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ophyd-async
3
- Version: 0.12.2
3
+ Version: 0.13.0
4
4
  Summary: Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango
5
5
  Author-email: Tom Cobb <tom.cobb@diamond.ac.uk>
6
6
  License: BSD 3-Clause License
@@ -49,10 +49,11 @@ Requires-Dist: colorlog
49
49
  Requires-Dist: pydantic>=2.0
50
50
  Requires-Dist: pydantic-numpy
51
51
  Requires-Dist: stamina>=23.1.0
52
+ Requires-Dist: scanspec>=1.0a1
52
53
  Provides-Extra: sim
53
54
  Requires-Dist: h5py; extra == "sim"
54
55
  Provides-Extra: ca
55
- Requires-Dist: aioca>=1.6; extra == "ca"
56
+ Requires-Dist: aioca>=2.0a4; extra == "ca"
56
57
  Provides-Extra: pva
57
58
  Requires-Dist: p4p>=4.2.0; extra == "pva"
58
59
  Provides-Extra: tango