ophyd-async 0.10.0a2__py3-none-any.whl → 0.10.0a4__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. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +4 -2
  3. ophyd_async/core/_derived_signal.py +42 -14
  4. ophyd_async/core/_derived_signal_backend.py +4 -4
  5. ophyd_async/core/_detector.py +71 -57
  6. ophyd_async/core/_device.py +3 -3
  7. ophyd_async/core/_hdf_dataset.py +1 -5
  8. ophyd_async/core/_providers.py +0 -8
  9. ophyd_async/core/_readable.py +13 -1
  10. ophyd_async/core/_signal.py +21 -5
  11. ophyd_async/core/_signal_backend.py +18 -8
  12. ophyd_async/core/_utils.py +31 -14
  13. ophyd_async/epics/adandor/_andor_controller.py +1 -1
  14. ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
  15. ophyd_async/epics/adcore/_core_detector.py +2 -2
  16. ophyd_async/epics/adcore/_core_io.py +3 -3
  17. ophyd_async/epics/adcore/_core_logic.py +3 -3
  18. ophyd_async/epics/adcore/_core_writer.py +22 -29
  19. ophyd_async/epics/adcore/_hdf_writer.py +17 -15
  20. ophyd_async/epics/adcore/_jpeg_writer.py +1 -3
  21. ophyd_async/epics/adcore/_tiff_writer.py +1 -3
  22. ophyd_async/epics/adcore/_utils.py +11 -2
  23. ophyd_async/epics/adkinetix/_kinetix_controller.py +1 -1
  24. ophyd_async/epics/adpilatus/_pilatus.py +1 -1
  25. ophyd_async/epics/adpilatus/_pilatus_controller.py +6 -13
  26. ophyd_async/epics/adpilatus/_pilatus_io.py +1 -1
  27. ophyd_async/epics/advimba/_vimba_controller.py +1 -1
  28. ophyd_async/epics/core/_aioca.py +2 -2
  29. ophyd_async/epics/core/_p4p.py +1 -1
  30. ophyd_async/epics/core/_pvi_connector.py +5 -3
  31. ophyd_async/epics/core/_util.py +21 -13
  32. ophyd_async/epics/eiger/__init__.py +2 -4
  33. ophyd_async/epics/eiger/_odin_io.py +58 -36
  34. ophyd_async/epics/motor.py +3 -2
  35. ophyd_async/epics/testing/_example_ioc.py +1 -0
  36. ophyd_async/epics/testing/test_records.db +5 -0
  37. ophyd_async/fastcs/eiger/__init__.py +13 -0
  38. ophyd_async/{epics → fastcs}/eiger/_eiger.py +15 -6
  39. ophyd_async/{epics → fastcs}/eiger/_eiger_controller.py +17 -27
  40. ophyd_async/fastcs/eiger/_eiger_io.py +54 -0
  41. ophyd_async/fastcs/panda/_block.py +2 -0
  42. ophyd_async/fastcs/panda/_hdf_panda.py +0 -1
  43. ophyd_async/fastcs/panda/_writer.py +23 -22
  44. ophyd_async/plan_stubs/_fly.py +2 -2
  45. ophyd_async/sim/_blob_detector.py +0 -1
  46. ophyd_async/sim/_blob_detector_controller.py +1 -1
  47. ophyd_async/sim/_blob_detector_writer.py +15 -19
  48. ophyd_async/sim/_motor.py +2 -2
  49. ophyd_async/sim/_pattern_generator.py +2 -0
  50. ophyd_async/tango/core/_base_device.py +2 -1
  51. ophyd_async/tango/core/_converters.py +2 -6
  52. ophyd_async/tango/core/_signal.py +8 -8
  53. ophyd_async/tango/core/_tango_transport.py +12 -12
  54. ophyd_async/tango/demo/_tango/_servers.py +0 -1
  55. ophyd_async/tango/testing/_one_of_everything.py +2 -2
  56. {ophyd_async-0.10.0a2.dist-info → ophyd_async-0.10.0a4.dist-info}/METADATA +1 -1
  57. {ophyd_async-0.10.0a2.dist-info → ophyd_async-0.10.0a4.dist-info}/RECORD +60 -59
  58. {ophyd_async-0.10.0a2.dist-info → ophyd_async-0.10.0a4.dist-info}/WHEEL +1 -1
  59. ophyd_async/epics/eiger/_eiger_io.py +0 -42
  60. {ophyd_async-0.10.0a2.dist-info → ophyd_async-0.10.0a4.dist-info}/licenses/LICENSE +0 -0
  61. {ophyd_async-0.10.0a2.dist-info → ophyd_async-0.10.0a4.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,20 @@
1
- from collections.abc import Sequence
2
- from typing import Any, get_args, get_origin
1
+ from collections.abc import Mapping, Sequence
2
+ from typing import Any, TypeVar, get_args, get_origin
3
3
 
4
4
  import numpy as np
5
5
 
6
6
  from ophyd_async.core import (
7
7
  SignalBackend,
8
8
  SignalDatatypeT,
9
+ StrictEnum,
9
10
  SubsetEnum,
11
+ SupersetEnum,
10
12
  get_dtype,
11
13
  get_enum_cls,
12
14
  )
13
15
 
16
+ T = TypeVar("T")
17
+
14
18
 
15
19
  def get_pv_basename_and_field(pv: str) -> tuple[str, str | None]:
16
20
  """Split PV into record name and field."""
@@ -23,26 +27,30 @@ def get_pv_basename_and_field(pv: str) -> tuple[str, str | None]:
23
27
 
24
28
  def get_supported_values(
25
29
  pv: str,
26
- datatype: type,
30
+ datatype: type[T],
27
31
  pv_choices: Sequence[str],
28
- ) -> dict[str, str]:
32
+ ) -> Mapping[str, T | str]:
29
33
  enum_cls = get_enum_cls(datatype)
30
34
  if not enum_cls:
31
35
  raise TypeError(f"{datatype} is not an Enum")
32
36
  choices = [v.value for v in enum_cls]
33
37
  error_msg = f"{pv} has choices {pv_choices}, but {datatype} requested {choices} "
34
- if issubclass(enum_cls, SubsetEnum):
38
+ if issubclass(enum_cls, StrictEnum):
39
+ if set(choices) != set(pv_choices):
40
+ raise TypeError(error_msg + "to be strictly equal to them.")
41
+ elif issubclass(enum_cls, SubsetEnum):
35
42
  if not set(choices).issubset(pv_choices):
36
43
  raise TypeError(error_msg + "to be a subset of them.")
44
+ elif issubclass(enum_cls, SupersetEnum):
45
+ if not set(pv_choices).issubset(choices):
46
+ raise TypeError(error_msg + "to be a superset of them.")
37
47
  else:
38
- if set(choices) != set(pv_choices):
39
- raise TypeError(error_msg + "to be strictly equal to them.")
40
-
41
- # Take order from the pv choices
42
- supported_values = {x: x for x in pv_choices}
43
- # But override those that we specify via the datatype
44
- for v in enum_cls:
45
- supported_values[v.value] = v
48
+ raise TypeError(f"{datatype} is not a StrictEnum, SubsetEnum, or SupersetEnum")
49
+ # Create a map from the string value to the enum instance
50
+ # For StrictEnum and SupersetEnum, all values here will be enum values
51
+ # For SubsetEnum, only the values in choices will be enum values, the rest will be
52
+ # strings
53
+ supported_values = {x: enum_cls(x) for x in pv_choices}
46
54
  return supported_values
47
55
 
48
56
 
@@ -1,5 +1,3 @@
1
- from ._eiger import EigerDetector, EigerTriggerInfo
2
- from ._eiger_controller import EigerController
3
- from ._eiger_io import EigerDriverIO
1
+ from ._odin_io import Odin, OdinWriter, Writing
4
2
 
5
- __all__ = ["EigerDetector", "EigerController", "EigerDriverIO", "EigerTriggerInfo"]
3
+ __all__ = ["Odin", "OdinWriter", "Writing"]
@@ -2,17 +2,20 @@ import asyncio
2
2
  from collections.abc import AsyncGenerator, AsyncIterator
3
3
 
4
4
  from bluesky.protocols import StreamAsset
5
- from event_model import DataKey
5
+ from event_model import DataKey # type: ignore
6
6
 
7
7
  from ophyd_async.core import (
8
+ DEFAULT_TIMEOUT,
8
9
  DetectorWriter,
9
10
  Device,
10
11
  DeviceVector,
11
- NameProvider,
12
12
  PathProvider,
13
+ Reference,
14
+ SignalR,
13
15
  StrictEnum,
14
16
  observe_value,
15
17
  set_and_wait_for_value,
18
+ wait_for_value,
16
19
  )
17
20
  from ophyd_async.epics.core import (
18
21
  epics_signal_r,
@@ -22,44 +25,53 @@ from ophyd_async.epics.core import (
22
25
 
23
26
 
24
27
  class Writing(StrictEnum):
25
- ON = "ON"
26
- OFF = "OFF"
28
+ CAPTURE = "Capture"
29
+ DONE = "Done"
27
30
 
28
31
 
29
32
  class OdinNode(Device):
30
33
  def __init__(self, prefix: str, name: str = "") -> None:
31
- self.writing = epics_signal_r(Writing, f"{prefix}HDF:Writing")
32
- self.connected = epics_signal_r(bool, f"{prefix}Connected")
34
+ self.writing = epics_signal_r(str, f"{prefix}Writing_RBV")
35
+ self.frames_dropped = epics_signal_r(int, f"{prefix}FramesDropped_RBV")
36
+ self.frames_time_out = epics_signal_r(int, f"{prefix}FramesTimedOut_RBV")
37
+ self.error_status = epics_signal_r(str, f"{prefix}FPErrorState_RBV")
38
+ self.fp_initialised = epics_signal_r(int, f"{prefix}FPProcessConnected_RBV")
39
+ self.fr_initialised = epics_signal_r(int, f"{prefix}FRProcessConnected_RBV")
40
+ self.num_captured = epics_signal_r(int, f"{prefix}NumCaptured_RBV")
41
+ self.clear_errors = epics_signal_rw(int, f"{prefix}FPClearErrors")
42
+ self.error_message = epics_signal_rw(str, f"{prefix}FPErrorMessage_RBV")
33
43
 
34
44
  super().__init__(name)
35
45
 
36
46
 
37
47
  class Odin(Device):
38
48
  def __init__(self, prefix: str, name: str = "") -> None:
39
- self.nodes = DeviceVector({i: OdinNode(f"{prefix}FP{i}:") for i in range(4)})
40
-
41
- self.capture = epics_signal_rw(
42
- Writing, f"{prefix}Writing", f"{prefix}ConfigHdfWrite"
49
+ self.nodes = DeviceVector(
50
+ {i: OdinNode(f"{prefix[:-1]}{i + 1}:") for i in range(4)}
43
51
  )
44
- self.num_captured = epics_signal_r(int, f"{prefix}FramesWritten")
45
- self.num_to_capture = epics_signal_rw_rbv(int, f"{prefix}ConfigHdfFrames")
46
52
 
47
- self.start_timeout = epics_signal_rw_rbv(int, f"{prefix}TimeoutTimerPeriod")
53
+ self.capture = epics_signal_rw(Writing, f"{prefix}Capture")
54
+ self.capture_rbv = epics_signal_r(str, prefix + "Capture_RBV")
55
+ self.num_captured = epics_signal_r(int, f"{prefix}NumCaptured_RBV")
56
+ self.num_to_capture = epics_signal_rw_rbv(int, f"{prefix}NumCapture")
48
57
 
49
- self.image_height = epics_signal_rw_rbv(int, f"{prefix}DatasetDataDims0")
50
- self.image_width = epics_signal_rw_rbv(int, f"{prefix}DatasetDataDims1")
58
+ self.start_timeout = epics_signal_rw(str, f"{prefix}StartTimeout")
59
+ self.timeout_active_rbv = epics_signal_r(str, f"{prefix}TimeoutActive_RBV")
51
60
 
52
- self.num_row_chunks = epics_signal_rw_rbv(int, f"{prefix}DatasetDataChunks1")
53
- self.num_col_chunks = epics_signal_rw_rbv(int, f"{prefix}DatasetDataChunks2")
61
+ self.image_height = epics_signal_rw_rbv(int, f"{prefix}ImageHeight")
62
+ self.image_width = epics_signal_rw_rbv(int, f"{prefix}ImageWidth")
54
63
 
55
- self.file_path = epics_signal_rw_rbv(str, f"{prefix}ConfigHdfFilePath")
56
- self.file_name = epics_signal_rw_rbv(str, f"{prefix}ConfigHdfFilePrefix")
64
+ self.num_row_chunks = epics_signal_rw_rbv(int, f"{prefix}NumRowChunks")
65
+ self.num_col_chunks = epics_signal_rw_rbv(int, f"{prefix}NumColChunks")
57
66
 
58
- self.acquisition_id = epics_signal_rw_rbv(
59
- str, f"{prefix}ConfigHdfAcquisitionId"
60
- )
67
+ self.file_path = epics_signal_rw_rbv(str, f"{prefix}FilePath")
68
+ self.file_name = epics_signal_rw_rbv(str, f"{prefix}FileName")
61
69
 
62
- self.data_type = epics_signal_rw_rbv(str, f"{prefix}DatasetDataDatatype")
70
+ self.num_frames_chunks = epics_signal_rw(int, prefix + "NumFramesChunks")
71
+ self.meta_active = epics_signal_r(str, prefix + "META:AcquisitionActive_RBV")
72
+ self.meta_writing = epics_signal_r(str, prefix + "META:Writing_RBV")
73
+
74
+ self.data_type = epics_signal_rw_rbv(str, f"{prefix}DataType")
63
75
 
64
76
  super().__init__(name)
65
77
 
@@ -68,27 +80,35 @@ class OdinWriter(DetectorWriter):
68
80
  def __init__(
69
81
  self,
70
82
  path_provider: PathProvider,
71
- name_provider: NameProvider,
72
83
  odin_driver: Odin,
84
+ eiger_bit_depth: SignalR[int],
73
85
  ) -> None:
74
86
  self._drv = odin_driver
75
87
  self._path_provider = path_provider
76
- self._name_provider = name_provider
88
+ self._eiger_bit_depth = Reference(eiger_bit_depth)
77
89
  super().__init__()
78
90
 
79
- async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
80
- info = self._path_provider(device_name=self._name_provider())
91
+ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]:
92
+ info = self._path_provider(device_name=name)
93
+ self._exposures_per_event = exposures_per_event
81
94
 
82
95
  await asyncio.gather(
83
96
  self._drv.file_path.set(str(info.directory_path)),
84
97
  self._drv.file_name.set(info.filename),
85
- self._drv.data_type.set(
86
- "uint16"
87
- ), # TODO: Get from eiger https://github.com/bluesky/ophyd-async/issues/529
98
+ self._drv.data_type.set(f"UInt{await self._eiger_bit_depth().get_value()}"),
88
99
  self._drv.num_to_capture.set(0),
89
100
  )
90
101
 
91
- await self._drv.capture.set(Writing.ON)
102
+ await wait_for_value(self._drv.meta_active, "Active", timeout=DEFAULT_TIMEOUT)
103
+
104
+ await self._drv.capture.set(
105
+ Writing.CAPTURE, wait=False
106
+ ) # TODO: Investigate why we do not get a put callback when setting capture pv https://github.com/bluesky/ophyd-async/issues/866
107
+
108
+ await asyncio.gather(
109
+ wait_for_value(self._drv.capture_rbv, "Capturing", timeout=DEFAULT_TIMEOUT),
110
+ wait_for_value(self._drv.meta_writing, "Writing", timeout=DEFAULT_TIMEOUT),
111
+ )
92
112
 
93
113
  return await self._describe()
94
114
 
@@ -100,7 +120,7 @@ class OdinWriter(DetectorWriter):
100
120
  return {
101
121
  "data": DataKey(
102
122
  source=self._drv.file_name.source,
103
- shape=list(data_shape),
123
+ shape=[self._exposures_per_event, *data_shape],
104
124
  dtype="array",
105
125
  # TODO: Use correct type based on eiger https://github.com/bluesky/ophyd-async/issues/529
106
126
  dtype_numpy="<u2",
@@ -112,14 +132,16 @@ class OdinWriter(DetectorWriter):
112
132
  self, timeout: float
113
133
  ) -> AsyncGenerator[int, None]:
114
134
  async for num_captured in observe_value(self._drv.num_captured, timeout):
115
- yield num_captured
135
+ yield num_captured // self._exposures_per_event
116
136
 
117
137
  async def get_indices_written(self) -> int:
118
- return await self._drv.num_captured.get_value()
138
+ return await self._drv.num_captured.get_value() // self._exposures_per_event
119
139
 
120
- def collect_stream_docs(self, indices_written: int) -> AsyncIterator[StreamAsset]:
140
+ def collect_stream_docs(
141
+ self, name: str, indices_written: int
142
+ ) -> AsyncIterator[StreamAsset]:
121
143
  # TODO: Correctly return stream https://github.com/bluesky/ophyd-async/issues/530
122
144
  raise NotImplementedError()
123
145
 
124
146
  async def close(self) -> None:
125
- await set_and_wait_for_value(self._drv.capture, Writing.OFF)
147
+ await set_and_wait_for_value(self._drv.capture, Writing.DONE)
@@ -260,10 +260,11 @@ class Motor(
260
260
  (await self.acceleration_time.get_value()) * fly_velocity * 0.5
261
261
  )
262
262
 
263
- self._fly_completed_position = end_position + run_up_distance
263
+ direction = 1 if end_position > start_position else -1
264
+ self._fly_completed_position = end_position + (run_up_distance * direction)
264
265
 
265
266
  # Prepared position not used after prepare, so no need to store in self
266
- fly_prepared_position = start_position - run_up_distance
267
+ fly_prepared_position = start_position - (run_up_distance * direction)
267
268
 
268
269
  motor_lower_limit, motor_upper_limit, egu = await asyncio.gather(
269
270
  self.low_limit_travel.get_value(),
@@ -48,6 +48,7 @@ class EpicsTestCaDevice(EpicsDevice):
48
48
  longstr: A[SignalRW[str], PvSuffix("longstr")]
49
49
  longstr2: A[SignalRW[str], PvSuffix("longstr2.VAL$")]
50
50
  a_bool: A[SignalRW[bool], PvSuffix("bool")]
51
+ slowseq: A[SignalRW[int], PvSuffix("slowseq")]
51
52
  enum: A[SignalRW[EpicsTestEnum], PvSuffix("enum")]
52
53
  enum2: A[SignalRW[EpicsTestEnum], PvSuffix("enum2")]
53
54
  subset_enum: A[SignalRW[EpicsTestSubsetEnum], PvSuffix("subset_enum")]
@@ -112,6 +112,11 @@ record(mbbo, "$(device)enum_str_fallback") {
112
112
  field(PINI, "YES")
113
113
  }
114
114
 
115
+ record(seq, "$(device)slowseq") {
116
+ field(DLY1, "0.5")
117
+ field(LNK1, "$(device)slowseq.DESC")
118
+ }
119
+
115
120
  record(waveform, "$(device)uint8a") {
116
121
  field(NELM, "3")
117
122
  field(FTVL, "UCHAR")
@@ -0,0 +1,13 @@
1
+ from ._eiger import EigerDetector, EigerTriggerInfo
2
+ from ._eiger_controller import EigerController
3
+ from ._eiger_io import EigerDetectorIO, EigerDriverIO, EigerMonitorIO, EigerStreamIO
4
+
5
+ __all__ = [
6
+ "EigerDetector",
7
+ "EigerController",
8
+ "EigerDriverIO",
9
+ "EigerTriggerInfo",
10
+ "EigerDetectorIO",
11
+ "EigerMonitorIO",
12
+ "EigerStreamIO",
13
+ ]
@@ -1,10 +1,15 @@
1
1
  from pydantic import Field
2
2
 
3
- from ophyd_async.core import AsyncStatus, PathProvider, StandardDetector, TriggerInfo
3
+ from ophyd_async.core import (
4
+ AsyncStatus,
5
+ PathProvider,
6
+ StandardDetector,
7
+ TriggerInfo,
8
+ )
9
+ from ophyd_async.epics.eiger import Odin, OdinWriter
4
10
 
5
11
  from ._eiger_controller import EigerController
6
12
  from ._eiger_io import EigerDriverIO
7
- from ._odin_io import Odin, OdinWriter
8
13
 
9
14
 
10
15
  class EigerTriggerInfo(TriggerInfo):
@@ -15,22 +20,26 @@ class EigerDetector(StandardDetector):
15
20
  """Ophyd-async implementation of an Eiger Detector."""
16
21
 
17
22
  _controller: EigerController
18
- _writer: Odin
23
+ _writer: OdinWriter
19
24
 
20
25
  def __init__(
21
26
  self,
22
27
  prefix: str,
23
28
  path_provider: PathProvider,
24
29
  drv_suffix="-EA-EIGER-01:",
25
- hdf_suffix="-EA-ODIN-01:",
30
+ hdf_suffix="-EA-EIGER-01:OD:",
26
31
  name="",
27
32
  ):
28
33
  self.drv = EigerDriverIO(prefix + drv_suffix)
29
- self.odin = Odin(prefix + hdf_suffix + "FP:")
34
+ self.odin = Odin(prefix + hdf_suffix)
30
35
 
31
36
  super().__init__(
32
37
  EigerController(self.drv),
33
- OdinWriter(path_provider, lambda: self.name, self.odin),
38
+ OdinWriter(
39
+ path_provider,
40
+ self.odin,
41
+ self.drv.detector.bit_depth_readout,
42
+ ),
34
43
  name=name,
35
44
  )
36
45
 
@@ -2,11 +2,10 @@ import asyncio
2
2
 
3
3
  from ophyd_async.core import (
4
4
  DEFAULT_TIMEOUT,
5
- AsyncStatus,
6
5
  DetectorController,
7
6
  DetectorTrigger,
8
7
  TriggerInfo,
9
- set_and_wait_for_other_value,
8
+ wait_for_value,
10
9
  )
11
10
 
12
11
  from ._eiger_io import EigerDriverIO, EigerTriggerMode
@@ -20,59 +19,50 @@ EIGER_TRIGGER_MODE_MAP = {
20
19
 
21
20
 
22
21
  class EigerController(DetectorController):
23
- """Controller for the Eiger detector."""
24
-
25
22
  def __init__(
26
23
  self,
27
24
  driver: EigerDriverIO,
28
25
  ) -> None:
29
26
  self._drv = driver
30
- self._arm_status: AsyncStatus | None = None
31
27
 
32
28
  def get_deadtime(self, exposure: float | None) -> float:
33
29
  # See https://media.dectris.com/filer_public/30/14/3014704e-5f3b-43ba-8ccf-8ef720e60d2a/240202_usermanual_eiger2.pdf
34
30
  return 0.0001
35
31
 
36
32
  async def set_energy(self, energy: float, tolerance: float = 0.1):
37
- """Change photon energy if outside tolerance.
33
+ """Set photon energy."""
34
+ """Changing photon energy takes some time so only do so if the current energy is
35
+ outside the tolerance."""
38
36
 
39
- It takes some time so don't do it unless it is outside tolerance.
40
- """
41
- current_energy = await self._drv.photon_energy.get_value()
37
+ current_energy = await self._drv.detector.photon_energy.get_value()
42
38
  if abs(current_energy - energy) > tolerance:
43
- await self._drv.photon_energy.set(energy)
39
+ await self._drv.detector.photon_energy.set(energy)
44
40
 
45
41
  async def prepare(self, trigger_info: TriggerInfo):
46
42
  coros = [
47
- self._drv.trigger_mode.set(
43
+ self._drv.detector.trigger_mode.set(
48
44
  EIGER_TRIGGER_MODE_MAP[trigger_info.trigger].value
49
45
  ),
50
- self._drv.num_images.set(trigger_info.total_number_of_triggers),
46
+ self._drv.detector.nimages.set(trigger_info.total_number_of_exposures),
51
47
  ]
52
48
  if trigger_info.livetime is not None:
53
49
  coros.extend(
54
50
  [
55
- self._drv.acquire_time.set(trigger_info.livetime),
56
- self._drv.acquire_period.set(trigger_info.livetime),
51
+ self._drv.detector.count_time.set(trigger_info.livetime),
52
+ self._drv.detector.frame_time.set(trigger_info.livetime),
57
53
  ]
58
54
  )
55
+
59
56
  await asyncio.gather(*coros)
60
57
 
61
58
  async def arm(self):
62
- # TODO: Detector state should be an enum see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
63
- self._arm_status = await set_and_wait_for_other_value(
64
- self._drv.arm,
65
- 1,
66
- self._drv.state,
67
- "ready",
68
- timeout=DEFAULT_TIMEOUT,
69
- wait_for_set_completion=False,
70
- )
59
+ # NOTE: This will return immedietly on FastCS 0.8.0,
60
+ # but will return after the Eiger has completed arming in 0.9.0.
61
+ # https://github.com/DiamondLightSource/FastCS/pull/141
62
+ await self._drv.detector.arm.trigger(timeout=DEFAULT_TIMEOUT)
71
63
 
72
64
  async def wait_for_idle(self):
73
- if self._arm_status and not self._arm_status.done:
74
- await self._arm_status
75
- self._arm_status = None
65
+ await wait_for_value(self._drv.detector.state, "idle", timeout=DEFAULT_TIMEOUT)
76
66
 
77
67
  async def disarm(self):
78
- await self._drv.disarm.set(1)
68
+ await self._drv.detector.disarm.trigger()
@@ -0,0 +1,54 @@
1
+ from ophyd_async.core import (
2
+ Device,
3
+ SignalR,
4
+ SignalRW,
5
+ SignalX,
6
+ StrictEnum,
7
+ )
8
+ from ophyd_async.fastcs.core import fastcs_connector
9
+
10
+
11
+ class EigerTriggerMode(StrictEnum):
12
+ INTERNAL = "ints"
13
+ EDGE = "exts"
14
+ GATE = "exte"
15
+
16
+
17
+ class EigerMonitorIO(Device):
18
+ pass
19
+
20
+
21
+ class EigerStreamIO(Device):
22
+ pass
23
+
24
+
25
+ class EigerDetectorIO(Device):
26
+ bit_depth_readout: SignalR[int]
27
+ state: SignalR[str]
28
+ count_time: SignalRW[float]
29
+ frame_time: SignalRW[float]
30
+ nimages: SignalRW[int]
31
+ nexpi: SignalRW[int]
32
+ trigger_mode: SignalRW[str]
33
+ roi_mode: SignalRW[str]
34
+ photon_energy: SignalRW[float]
35
+ beam_center_x: SignalRW[float]
36
+ beam_center_y: SignalRW[float]
37
+ detector_distance: SignalRW[float]
38
+ omega_start: SignalRW[float]
39
+ omega_increment: SignalRW[float]
40
+ arm: SignalX
41
+ disarm: SignalX
42
+ trigger: SignalX
43
+
44
+
45
+ class EigerDriverIO(Device):
46
+ """Contains signals for handling IO on the Eiger detector."""
47
+
48
+ stale_parameters: SignalR[bool]
49
+ monitor: EigerMonitorIO
50
+ stream: EigerStreamIO
51
+ detector: EigerDetectorIO
52
+
53
+ def __init__(self, uri: str, name: str = ""):
54
+ super().__init__(name=name, connector=fastcs_connector(self, uri))
@@ -38,6 +38,8 @@ class PulseBlock(Device):
38
38
  """Used for configuring pulses in the PandA."""
39
39
 
40
40
  delay: SignalRW[float]
41
+ pulses: SignalRW[int]
42
+ step: SignalRW[float]
41
43
  width: SignalRW[float]
42
44
 
43
45
 
@@ -30,7 +30,6 @@ class HDFPanda(
30
30
  controller = PandaPcapController(pcap=self.pcap)
31
31
  writer = PandaHDFWriter(
32
32
  path_provider=path_provider,
33
- name_provider=lambda: name,
34
33
  panda_data_block=self.data,
35
34
  )
36
35
  super().__init__(
@@ -10,7 +10,6 @@ from ophyd_async.core import (
10
10
  DetectorWriter,
11
11
  HDFDatasetDescription,
12
12
  HDFDocumentComposer,
13
- NameProvider,
14
13
  PathProvider,
15
14
  observe_value,
16
15
  wait_for_value,
@@ -25,24 +24,22 @@ class PandaHDFWriter(DetectorWriter):
25
24
  def __init__(
26
25
  self,
27
26
  path_provider: PathProvider,
28
- name_provider: NameProvider,
29
27
  panda_data_block: DataBlock,
30
28
  ) -> None:
31
29
  self.panda_data_block = panda_data_block
32
30
  self._path_provider = path_provider
33
- self._name_provider = name_provider
34
31
  self._datasets: list[HDFDatasetDescription] = []
35
32
  self._composer: HDFDocumentComposer | None = None
36
- self._multiplier = 1
37
33
 
38
34
  # Triggered on PCAP arm
39
- async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
35
+ async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]:
40
36
  """Retrieve and get descriptor of all PandA signals marked for capture."""
37
+ self._exposures_per_event = exposures_per_event
41
38
  # Ensure flushes are immediate
42
39
  await self.panda_data_block.flush_period.set(0)
43
40
 
44
41
  self._composer = None
45
- info = self._path_provider(device_name=self._name_provider())
42
+ info = self._path_provider(device_name=name)
46
43
 
47
44
  # Set create dir depth first to guarantee that callback when setting
48
45
  # directory path has correct value
@@ -66,21 +63,19 @@ class PandaHDFWriter(DetectorWriter):
66
63
 
67
64
  # Wait for it to start, stashing the status that tells us when it finishes
68
65
  await self.panda_data_block.capture.set(True)
69
- if multiplier > 1:
70
- raise ValueError(
71
- "All PandA datasets should be scalar, multiplier should be 1"
72
- )
73
66
 
74
- return await self._describe()
67
+ return await self._describe(name)
75
68
 
76
- async def _describe(self) -> dict[str, DataKey]:
69
+ async def _describe(self, name: str) -> dict[str, DataKey]:
77
70
  """Return a describe based on the datasets PV."""
78
- await self._update_datasets()
71
+ await self._update_datasets(name)
79
72
  describe = {
80
73
  ds.data_key: DataKey(
81
74
  source=self.panda_data_block.hdf_directory.source,
82
75
  shape=list(ds.shape),
83
- dtype="number",
76
+ dtype="array"
77
+ if self._exposures_per_event > 1 or len(ds.shape) > 1
78
+ else "number",
84
79
  # PandA data should always be written as Float64
85
80
  dtype_numpy=ds.dtype_numpy,
86
81
  external="STREAM:",
@@ -89,7 +84,7 @@ class PandaHDFWriter(DetectorWriter):
89
84
  }
90
85
  return describe
91
86
 
92
- async def _update_datasets(self) -> None:
87
+ async def _update_datasets(self, name: str) -> None:
93
88
  # Load data from the datasets PV on the panda, update internal
94
89
  # representation of datasets that the panda will write.
95
90
  capture_table = await self.panda_data_block.datasets.get_value()
@@ -99,9 +94,10 @@ class PandaHDFWriter(DetectorWriter):
99
94
  HDFDatasetDescription(
100
95
  data_key=dataset_name,
101
96
  dataset="/" + dataset_name,
102
- shape=(),
97
+ shape=(self._exposures_per_event,)
98
+ if self._exposures_per_event > 1
99
+ else (),
103
100
  dtype_numpy="<f8",
104
- multiplier=1,
105
101
  chunk_shape=(1024,),
106
102
  )
107
103
  for dataset_name in capture_table.name
@@ -111,7 +107,7 @@ class PandaHDFWriter(DetectorWriter):
111
107
  # i.e. no stream resources will be generated
112
108
  if len(self._datasets) == 0:
113
109
  self.panda_data_block.log.warning(
114
- f"PandA {self._name_provider()} DATASETS table is empty! "
110
+ f"PandA {name} DATASETS table is empty! "
115
111
  "No stream resource docs will be generated. "
116
112
  "Make sure captured positions have their corresponding "
117
113
  "*:DATASET PV set to a scientifically relevant name."
@@ -121,7 +117,9 @@ class PandaHDFWriter(DetectorWriter):
121
117
  # StandardDetector behavior
122
118
  async def wait_for_index(self, index: int, timeout: float | None = DEFAULT_TIMEOUT):
123
119
  def matcher(value: int) -> bool:
124
- return value >= index
120
+ # Index is already divided by exposures_per_event, so we need to also
121
+ # divide the value by exposures_per_event to get the correct index
122
+ return value // self._exposures_per_event >= index
125
123
 
126
124
  matcher.__name__ = f"index_at_least_{index}"
127
125
  await wait_for_value(
@@ -129,7 +127,10 @@ class PandaHDFWriter(DetectorWriter):
129
127
  )
130
128
 
131
129
  async def get_indices_written(self) -> int:
132
- return await self.panda_data_block.num_captured.get_value()
130
+ return (
131
+ await self.panda_data_block.num_captured.get_value()
132
+ // self._exposures_per_event
133
+ )
133
134
 
134
135
  async def observe_indices_written(
135
136
  self, timeout: float
@@ -138,10 +139,10 @@ class PandaHDFWriter(DetectorWriter):
138
139
  async for num_captured in observe_value(
139
140
  self.panda_data_block.num_captured, timeout
140
141
  ):
141
- yield num_captured // self._multiplier
142
+ yield num_captured // self._exposures_per_event
142
143
 
143
144
  async def collect_stream_docs(
144
- self, indices_written: int
145
+ self, name: str, indices_written: int
145
146
  ) -> AsyncIterator[StreamAsset]:
146
147
  # TODO: fail if we get dropped frames
147
148
  if indices_written:
@@ -60,11 +60,11 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
60
60
  deadtime = max(det._controller.get_deadtime(exposure) for det in detectors) # noqa: SLF001
61
61
 
62
62
  trigger_info = TriggerInfo(
63
- number_of_triggers=number_of_frames * repeats,
63
+ number_of_events=number_of_frames * repeats,
64
64
  trigger=DetectorTrigger.CONSTANT_GATE,
65
65
  deadtime=deadtime,
66
66
  livetime=exposure,
67
- frame_timeout=frame_timeout,
67
+ exposure_timeout=frame_timeout,
68
68
  )
69
69
  trigger_time = number_of_frames * (exposure + deadtime)
70
70
  pre_delay = max(period - 2 * shutter_time - trigger_time, 0)
@@ -26,7 +26,6 @@ class SimBlobDetector(StandardDetector):
26
26
  writer=BlobDetectorWriter(
27
27
  pattern_generator=self.pattern_generator,
28
28
  path_provider=path_provider,
29
- name_provider=lambda: self.name,
30
29
  ),
31
30
  config_sigs=config_sigs,
32
31
  name=name,