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
@@ -5,7 +5,15 @@ import logging
5
5
  from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
6
6
  from dataclasses import dataclass
7
7
  from enum import Enum, EnumMeta
8
- from typing import Any, Generic, Literal, ParamSpec, TypeVar, get_args, get_origin
8
+ from typing import (
9
+ Any,
10
+ Generic,
11
+ Literal,
12
+ ParamSpec,
13
+ TypeVar,
14
+ get_args,
15
+ get_origin,
16
+ )
9
17
  from unittest.mock import Mock
10
18
 
11
19
  import numpy as np
@@ -19,20 +27,16 @@ DEFAULT_TIMEOUT = 10.0
19
27
  logger = logging.getLogger("ophyd_async")
20
28
 
21
29
 
22
- class StrictEnumMeta(EnumMeta):
23
- def __new__(metacls, *args, **kwargs):
24
- ret = super().__new__(metacls, *args, **kwargs)
30
+ class UppercaseNameEnumMeta(EnumMeta):
31
+ def __new__(cls, *args, **kwargs):
32
+ ret = super().__new__(cls, *args, **kwargs)
25
33
  lowercase_names = [x.name for x in ret if not x.name.isupper()] # type: ignore
26
34
  if lowercase_names:
27
35
  raise TypeError(f"Names {lowercase_names} should be uppercase")
28
36
  return ret
29
37
 
30
38
 
31
- class StrictEnum(str, Enum, metaclass=StrictEnumMeta):
32
- """All members should exist in the Backend, and there will be no extras."""
33
-
34
-
35
- class SubsetEnumMeta(StrictEnumMeta):
39
+ class AnyStringUppercaseNameEnumMeta(UppercaseNameEnumMeta):
36
40
  def __call__(self, value, *args, **kwargs): # type: ignore
37
41
  """Return given value if it is a string and not a member of the enum.
38
42
 
@@ -54,10 +58,21 @@ class SubsetEnumMeta(StrictEnumMeta):
54
58
  return super().__call__(value, *args, **kwargs)
55
59
 
56
60
 
57
- class SubsetEnum(StrictEnum, metaclass=SubsetEnumMeta):
61
+ class StrictEnum(str, Enum, metaclass=UppercaseNameEnumMeta):
62
+ """All members should exist in the Backend, and there will be no extras."""
63
+
64
+
65
+ class SubsetEnum(str, Enum, metaclass=AnyStringUppercaseNameEnumMeta):
58
66
  """All members should exist in the Backend, but there may be extras."""
59
67
 
60
68
 
69
+ class SupersetEnum(str, Enum, metaclass=UppercaseNameEnumMeta):
70
+ """Some members should exist in the Backend, and there should be no extras."""
71
+
72
+
73
+ EnumTypes = StrictEnum | SubsetEnum | SupersetEnum
74
+
75
+
61
76
  CALCULATE_TIMEOUT = "CALCULATE_TIMEOUT"
62
77
  """Sentinel used to implement ``myfunc(timeout=CalculateTimeout)``
63
78
 
@@ -207,10 +222,11 @@ def get_dtype(datatype: type) -> np.dtype:
207
222
  return np.dtype(get_args(get_args(datatype)[1])[0])
208
223
 
209
224
 
210
- def get_enum_cls(datatype: type | None) -> type[StrictEnum] | None:
225
+ def get_enum_cls(datatype: type | None) -> type[EnumTypes] | None:
211
226
  """Get the enum class from a datatype.
212
227
 
213
- :raises TypeError: if type is not a [](#StrictEnum) or [](#SubsetEnum) subclass
228
+ :raises TypeError: if type is not a [](#StrictEnum) or [](#SubsetEnum)
229
+ or [](#SupersetEnum) subclass
214
230
  ```python
215
231
  >>> from ophyd_async.core import StrictEnum
216
232
  >>> from collections.abc import Sequence
@@ -227,10 +243,11 @@ def get_enum_cls(datatype: type | None) -> type[StrictEnum] | None:
227
243
  if get_origin(datatype) is Sequence:
228
244
  datatype = get_args(datatype)[0]
229
245
  if datatype and issubclass(datatype, Enum):
230
- if not issubclass(datatype, StrictEnum):
246
+ if not issubclass(datatype, EnumTypes):
231
247
  raise TypeError(
232
248
  f"{datatype} should inherit from ophyd_async.core.SubsetEnum "
233
- "or ophyd_async.core.StrictEnum"
249
+ "or ophyd_async.core.StrictEnum "
250
+ "or ophyd_async.core.SupersetEnum."
234
251
  )
235
252
  return datatype
236
253
  return None
@@ -32,7 +32,7 @@ class Andor2Controller(adcore.ADBaseController[Andor2DriverIO]):
32
32
  await asyncio.gather(
33
33
  self.driver.trigger_mode.set(self._get_trigger_mode(trigger_info.trigger)),
34
34
  self.driver.num_images.set(
35
- trigger_info.total_number_of_triggers or _MAX_NUM_IMAGE
35
+ trigger_info.total_number_of_exposures or _MAX_NUM_IMAGE
36
36
  ),
37
37
  self.driver.image_mode.set(adcore.ADImageMode.MULTIPLE),
38
38
  )
@@ -35,11 +35,11 @@ class AravisController(adcore.ADBaseController[AravisDriverIO]):
35
35
  else:
36
36
  raise ValueError(f"ADAravis does not support {trigger_info.trigger}")
37
37
 
38
- if trigger_info.total_number_of_triggers == 0:
38
+ if trigger_info.total_number_of_exposures == 0:
39
39
  image_mode = adcore.ADImageMode.CONTINUOUS
40
40
  else:
41
41
  image_mode = adcore.ADImageMode.MULTIPLE
42
42
  await asyncio.gather(
43
- self.driver.num_images.set(trigger_info.total_number_of_triggers),
43
+ self.driver.num_images.set(trigger_info.total_number_of_exposures),
44
44
  self.driver.image_mode.set(image_mode),
45
45
  )
@@ -22,8 +22,8 @@ class AreaDetector(StandardDetector[ADBaseControllerT, ADWriter]):
22
22
  self.fileio = writer.fileio
23
23
 
24
24
  if plugins is not None:
25
- for name, plugin in plugins.items():
26
- setattr(self, name, plugin)
25
+ for plugin_name, plugin in plugins.items():
26
+ setattr(self, plugin_name, plugin)
27
27
 
28
28
  super().__init__(
29
29
  controller,
@@ -70,14 +70,14 @@ class NDPluginStatsIO(NDPluginBaseIO):
70
70
  # Basic statistics
71
71
  compute_statistics: A[SignalRW[bool], PvSuffix.rbv("ComputeStatistics")]
72
72
  bgd_width: A[SignalRW[int], PvSuffix.rbv("BgdWidth")]
73
- total_array: A[SignalRW[float], PvSuffix.rbv("TotalArray")]
73
+ total: A[SignalR[float], PvSuffix.rbv("Total")]
74
74
  # Centroid statistics
75
75
  compute_centroid: A[SignalRW[bool], PvSuffix.rbv("ComputeCentroid")]
76
76
  centroid_threshold: A[SignalRW[float], PvSuffix.rbv("CentroidThreshold")]
77
77
  # X and Y Profiles
78
78
  compute_profiles: A[SignalRW[bool], PvSuffix.rbv("ComputeProfiles")]
79
- profile_size_x: A[SignalRW[int], PvSuffix.rbv("ProfileSizeX")]
80
- profile_size_y: A[SignalRW[int], PvSuffix.rbv("ProfileSizeY")]
79
+ profile_size_x: A[SignalR[int], PvSuffix.rbv("ProfileSizeX")]
80
+ profile_size_y: A[SignalR[int], PvSuffix.rbv("ProfileSizeY")]
81
81
  cursor_x: A[SignalRW[int], PvSuffix.rbv("CursorX")]
82
82
  cursor_y: A[SignalRW[int], PvSuffix.rbv("CursorY")]
83
83
  # Array Histogram
@@ -33,7 +33,7 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
33
33
  driver: ADBaseIOT,
34
34
  good_states: frozenset[ADState] = DEFAULT_GOOD_STATES,
35
35
  ) -> None:
36
- self.driver = driver
36
+ self.driver: ADBaseIOT = driver
37
37
  self.good_states = good_states
38
38
  self.frame_timeout = DEFAULT_TIMEOUT
39
39
  self._arm_status: AsyncStatus | None = None
@@ -49,7 +49,7 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
49
49
  DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
50
50
  )
51
51
  await asyncio.gather(
52
- self.driver.num_images.set(trigger_info.total_number_of_triggers),
52
+ self.driver.num_images.set(trigger_info.total_number_of_exposures),
53
53
  self.driver.image_mode.set(ADImageMode.MULTIPLE),
54
54
  )
55
55
 
@@ -176,7 +176,7 @@ class ADBaseContAcqController(ADBaseController[ADBaseIO]):
176
176
  await asyncio.gather(
177
177
  self.cb_plugin.enable_callbacks.set(ADCallbacks.ENABLE),
178
178
  self.cb_plugin.pre_count.set(0),
179
- self.cb_plugin.post_count.set(trigger_info.total_number_of_triggers),
179
+ self.cb_plugin.post_count.set(trigger_info.total_number_of_exposures),
180
180
  self.cb_plugin.preset_trigger_count.set(1),
181
181
  self.cb_plugin.flush_on_soft_trg.set(NDCBFlushOnSoftTrgMode.ON_NEW_IMAGE),
182
182
  )
@@ -5,14 +5,15 @@ from typing import Generic, TypeVar, get_args
5
5
  from urllib.parse import urlunparse
6
6
 
7
7
  from bluesky.protocols import Hints, StreamAsset
8
- from event_model import (
8
+ from event_model import ( # type: ignore
9
9
  ComposeStreamResource,
10
10
  DataKey,
11
11
  StreamRange,
12
12
  )
13
+ from pydantic import PositiveInt
13
14
 
14
15
  from ophyd_async.core._detector import DetectorWriter
15
- from ophyd_async.core._providers import DatasetDescriber, NameProvider, PathProvider
16
+ from ophyd_async.core._providers import DatasetDescriber, PathProvider
16
17
  from ophyd_async.core._signal import (
17
18
  observe_value,
18
19
  set_and_wait_for_value,
@@ -44,7 +45,6 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
44
45
  self,
45
46
  fileio: NDFileIOT,
46
47
  path_provider: PathProvider,
47
- name_provider: NameProvider,
48
48
  dataset_describer: DatasetDescriber,
49
49
  file_extension: str = "",
50
50
  mimetype: str = "",
@@ -53,7 +53,6 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
53
53
  self._plugins = plugins or {}
54
54
  self.fileio = fileio
55
55
  self._path_provider = path_provider
56
- self._name_provider = name_provider
57
56
  self._dataset_describer = dataset_describer
58
57
  self._file_extension = file_extension
59
58
  self._mimetype = mimetype
@@ -61,7 +60,6 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
61
60
  self._emitted_resource = None
62
61
 
63
62
  self._capture_status: AsyncStatus | None = None
64
- self._multiplier = 1
65
63
  self._filename_template = "%s%s_%6.6d"
66
64
 
67
65
  @classmethod
@@ -81,18 +79,11 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
81
79
  fileio = fileio_cls(prefix + (fileio_suffix or cls.default_suffix))
82
80
  dataset_describer = ADBaseDatasetDescriber(dataset_source or fileio)
83
81
 
84
- def name_provider() -> str:
85
- if fileio.parent == "Not attached to a detector":
86
- raise RuntimeError("Initializing writer without parent detector!")
87
- return fileio.parent.name
88
-
89
- writer = cls(
90
- fileio, path_provider, name_provider, dataset_describer, plugins=plugins
91
- )
82
+ writer = cls(fileio, path_provider, dataset_describer, plugins=plugins)
92
83
  return writer
93
84
 
94
- async def begin_capture(self) -> None:
95
- info = self._path_provider(device_name=self._name_provider())
85
+ async def begin_capture(self, name: str) -> None:
86
+ info = self._path_provider(device_name=name)
96
87
 
97
88
  await self.fileio.enable_callbacks.set(ADCallbacks.ENABLE)
98
89
 
@@ -125,19 +116,21 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
125
116
  self.fileio.capture, True, wait_for_set_completion=False
126
117
  )
127
118
 
128
- async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
119
+ async def open(
120
+ self, name: str, exposures_per_event: PositiveInt = 1
121
+ ) -> dict[str, DataKey]:
129
122
  self._emitted_resource = None
130
123
  self._last_emitted = 0
131
- self._multiplier = multiplier
124
+ self._exposures_per_event = exposures_per_event
132
125
  frame_shape = await self._dataset_describer.shape()
133
126
  dtype_numpy = await self._dataset_describer.np_datatype()
134
127
 
135
- await self.begin_capture()
128
+ await self.begin_capture(name)
136
129
 
137
130
  describe = {
138
- self._name_provider(): DataKey(
131
+ name: DataKey(
139
132
  source=self.fileio.full_file_name.source,
140
- shape=list(frame_shape),
133
+ shape=[exposures_per_event, *frame_shape],
141
134
  dtype="array",
142
135
  dtype_numpy=dtype_numpy,
143
136
  external="STREAM:",
@@ -150,14 +143,14 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
150
143
  ) -> AsyncGenerator[int, None]:
151
144
  """Wait until a specific index is ready to be collected."""
152
145
  async for num_captured in observe_value(self.fileio.num_captured, timeout):
153
- yield num_captured // self._multiplier
146
+ yield num_captured // self._exposures_per_event
154
147
 
155
148
  async def get_indices_written(self) -> int:
156
149
  num_captured = await self.fileio.num_captured.get_value()
157
- return num_captured // self._multiplier
150
+ return num_captured // self._exposures_per_event
158
151
 
159
152
  async def collect_stream_docs(
160
- self, indices_written: int
153
+ self, name: str, indices_written: int
161
154
  ) -> AsyncIterator[StreamAsset]:
162
155
  if indices_written:
163
156
  if not self._emitted_resource:
@@ -183,13 +176,14 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
183
176
  self._emitted_resource = bundler_composer(
184
177
  mimetype=self._mimetype,
185
178
  uri=uri,
186
- data_key=self._name_provider(),
179
+ # TODO no reference to detector's name
180
+ data_key=name,
187
181
  parameters={
188
- # Assume that we always write 1 frame per file/chunk
182
+ # Assume that we always write 1 frame per file/chunk, this
183
+ # may change to self._exposures_per_event in the future
189
184
  "chunk_shape": (1, *frame_shape),
190
185
  # Include file template for reconstruction in consolidator
191
186
  "template": file_template,
192
- "multiplier": self._multiplier,
193
187
  },
194
188
  uid=None,
195
189
  validate=True,
@@ -218,6 +212,5 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
218
212
  await self._capture_status
219
213
  self._capture_status = None
220
214
 
221
- @property
222
- def hints(self) -> Hints:
223
- return {"fields": [self._name_provider()]}
215
+ def get_hints(self, name: str) -> Hints:
216
+ return {"fields": [name]}
@@ -5,12 +5,12 @@ from xml.etree import ElementTree as ET
5
5
 
6
6
  from bluesky.protocols import StreamAsset
7
7
  from event_model import DataKey
8
+ from pydantic import PositiveInt
8
9
 
9
10
  from ophyd_async.core import (
10
11
  DatasetDescriber,
11
12
  HDFDatasetDescription,
12
13
  HDFDocumentComposer,
13
- NameProvider,
14
14
  PathProvider,
15
15
  )
16
16
 
@@ -31,14 +31,12 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
31
31
  self,
32
32
  fileio: NDFileHDFIO,
33
33
  path_provider: PathProvider,
34
- name_provider: NameProvider,
35
34
  dataset_describer: DatasetDescriber,
36
35
  plugins: dict[str, NDPluginBaseIO] | None = None,
37
36
  ) -> None:
38
37
  super().__init__(
39
38
  fileio,
40
39
  path_provider,
41
- name_provider,
42
40
  dataset_describer,
43
41
  plugins=plugins,
44
42
  file_extension=".h5",
@@ -48,7 +46,9 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
48
46
  self._composer: HDFDocumentComposer | None = None
49
47
  self._filename_template = "%s%s"
50
48
 
51
- async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
49
+ async def open(
50
+ self, name: str, exposures_per_event: PositiveInt = 1
51
+ ) -> dict[str, DataKey]:
52
52
  self._composer = None
53
53
 
54
54
  # Setting HDF writer specific signals
@@ -64,13 +64,13 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
64
64
  )
65
65
 
66
66
  # Set common AD file plugin params, begin capturing
67
- await self.begin_capture()
67
+ await self.begin_capture(name)
68
68
 
69
- name = self._name_provider()
70
69
  detector_shape = await self._dataset_describer.shape()
71
70
  np_dtype = await self._dataset_describer.np_datatype()
72
- self._multiplier = multiplier
73
- outer_shape = (multiplier,) if multiplier > 1 else ()
71
+
72
+ # Used by the base class
73
+ self._exposures_per_event = exposures_per_event
74
74
 
75
75
  # Determine number of frames that will be saved per HDF chunk
76
76
  frames_per_chunk = await self.fileio.num_frames_chunks.get_value()
@@ -80,9 +80,8 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
80
80
  HDFDatasetDescription(
81
81
  data_key=name,
82
82
  dataset="/entry/data/data",
83
- shape=detector_shape,
83
+ shape=(exposures_per_event, *detector_shape),
84
84
  dtype_numpy=np_dtype,
85
- multiplier=multiplier,
86
85
  chunk_shape=(frames_per_chunk, *detector_shape),
87
86
  )
88
87
  ]
@@ -107,20 +106,23 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
107
106
  HDFDatasetDescription(
108
107
  data_key=data_key,
109
108
  dataset=f"/entry/instrument/NDAttributes/{data_key}",
110
- shape=(),
109
+ shape=(exposures_per_event,)
110
+ if exposures_per_event > 1
111
+ else (),
111
112
  dtype_numpy=np_datatype,
112
113
  # NDAttributes appear to always be configured with
113
114
  # this chunk size
114
115
  chunk_shape=(16384,),
115
- multiplier=multiplier,
116
116
  )
117
117
  )
118
118
 
119
119
  describe = {
120
120
  ds.data_key: DataKey(
121
121
  source=self.fileio.full_file_name.source,
122
- shape=list(outer_shape + tuple(ds.shape)),
123
- dtype="array" if ds.shape else "number",
122
+ shape=list(ds.shape),
123
+ dtype="array"
124
+ if exposures_per_event > 1 or len(ds.shape) > 1
125
+ else "number",
124
126
  dtype_numpy=ds.dtype_numpy,
125
127
  external="STREAM:",
126
128
  )
@@ -129,7 +131,7 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
129
131
  return describe
130
132
 
131
133
  async def collect_stream_docs(
132
- self, indices_written: int
134
+ self, name: str, indices_written: int
133
135
  ) -> AsyncIterator[StreamAsset]:
134
136
  # TODO: fail if we get dropped frames
135
137
  await self.fileio.flush_now.set(True)
@@ -1,4 +1,4 @@
1
- from ophyd_async.core import DatasetDescriber, NameProvider, PathProvider
1
+ from ophyd_async.core import DatasetDescriber, PathProvider
2
2
 
3
3
  from ._core_io import NDFileIO, NDPluginBaseIO
4
4
  from ._core_writer import ADWriter
@@ -11,14 +11,12 @@ class ADJPEGWriter(ADWriter[NDFileIO]):
11
11
  self,
12
12
  fileio: NDFileIO,
13
13
  path_provider: PathProvider,
14
- name_provider: NameProvider,
15
14
  dataset_describer: DatasetDescriber,
16
15
  plugins: dict[str, NDPluginBaseIO] | None = None,
17
16
  ) -> None:
18
17
  super().__init__(
19
18
  fileio,
20
19
  path_provider,
21
- name_provider,
22
20
  dataset_describer,
23
21
  plugins=plugins,
24
22
  file_extension=".jpg",
@@ -1,4 +1,4 @@
1
- from ophyd_async.core import DatasetDescriber, NameProvider, PathProvider
1
+ from ophyd_async.core import DatasetDescriber, PathProvider
2
2
 
3
3
  from ._core_io import NDFileIO, NDPluginBaseIO
4
4
  from ._core_writer import ADWriter
@@ -11,14 +11,12 @@ class ADTIFFWriter(ADWriter[NDFileIO]):
11
11
  self,
12
12
  fileio: NDFileIO,
13
13
  path_provider: PathProvider,
14
- name_provider: NameProvider,
15
14
  dataset_describer: DatasetDescriber,
16
15
  plugins: dict[str, NDPluginBaseIO] | None = None,
17
16
  ) -> None:
18
17
  super().__init__(
19
18
  fileio,
20
19
  path_provider,
21
- name_provider,
22
20
  dataset_describer,
23
21
  plugins=plugins,
24
22
  file_extension=".tiff",
@@ -7,11 +7,12 @@ from ophyd_async.core import (
7
7
  SignalRW,
8
8
  StrictEnum,
9
9
  SubsetEnum,
10
+ SupersetEnum,
10
11
  wait_for_value,
11
12
  )
12
13
 
13
14
 
14
- class ADBaseDataType(StrictEnum):
15
+ class ADBaseDataType(SupersetEnum):
15
16
  INT8 = "Int8"
16
17
  UINT8 = "UInt8"
17
18
  INT16 = "Int16"
@@ -22,6 +23,9 @@ class ADBaseDataType(StrictEnum):
22
23
  UINT64 = "UInt64"
23
24
  FLOAT32 = "Float32"
24
25
  FLOAT64 = "Float64"
26
+ # Driver database override will blank the enum string if it doesn't
27
+ # support a datatype
28
+ UNDEFINED = ""
25
29
 
26
30
 
27
31
  def convert_ad_dtype_to_np(ad_dtype: ADBaseDataType) -> str:
@@ -37,7 +41,12 @@ def convert_ad_dtype_to_np(ad_dtype: ADBaseDataType) -> str:
37
41
  ADBaseDataType.FLOAT32: "<f4",
38
42
  ADBaseDataType.FLOAT64: "<f8",
39
43
  }
40
- return ad_dtype_to_np_dtype[ad_dtype]
44
+ np_type = ad_dtype_to_np_dtype.get(ad_dtype)
45
+ if np_type is None:
46
+ raise ValueError(
47
+ "Areadetector driver has a blank DataType, this is not supported"
48
+ )
49
+ return np_type
41
50
 
42
51
 
43
52
  def convert_pv_dtype_to_np(datatype: str) -> str:
@@ -34,7 +34,7 @@ class KinetixController(adcore.ADBaseController[KinetixDriverIO]):
34
34
  self.driver.trigger_mode.set(
35
35
  KINETIX_TRIGGER_MODE_MAP[trigger_info.trigger]
36
36
  ),
37
- self.driver.num_images.set(trigger_info.total_number_of_triggers),
37
+ self.driver.num_images.set(trigger_info.total_number_of_exposures),
38
38
  self.driver.image_mode.set(adcore.ADImageMode.MULTIPLE),
39
39
  )
40
40
  if trigger_info.livetime is not None and trigger_info.trigger not in [
@@ -27,7 +27,7 @@ class PilatusDetector(AreaDetector[PilatusController]):
27
27
  config_sigs: Sequence[SignalR] = (),
28
28
  ):
29
29
  driver = PilatusDriverIO(prefix + drv_suffix)
30
- controller = PilatusController(driver)
30
+ controller = PilatusController(driver, readout_time=readout_time)
31
31
 
32
32
  writer = writer_cls.with_io(
33
33
  prefix,
@@ -29,6 +29,7 @@ class PilatusController(adcore.ADBaseController[PilatusDriverIO]):
29
29
  DetectorTrigger.INTERNAL: PilatusTriggerMode.INTERNAL,
30
30
  DetectorTrigger.CONSTANT_GATE: PilatusTriggerMode.EXT_ENABLE,
31
31
  DetectorTrigger.VARIABLE_GATE: PilatusTriggerMode.EXT_ENABLE,
32
+ DetectorTrigger.EDGE_TRIGGER: PilatusTriggerMode.EXT_TRIGGER,
32
33
  }
33
34
 
34
35
  def __init__(
@@ -49,11 +50,13 @@ class PilatusController(adcore.ADBaseController[PilatusDriverIO]):
49
50
  trigger_info.livetime
50
51
  )
51
52
  await asyncio.gather(
52
- self.driver.trigger_mode.set(self._get_trigger_mode(trigger_info.trigger)),
53
+ self.driver.trigger_mode.set(
54
+ self._supported_trigger_types[trigger_info.trigger]
55
+ ),
53
56
  self.driver.num_images.set(
54
57
  999_999
55
- if trigger_info.total_number_of_triggers == 0
56
- else trigger_info.total_number_of_triggers
58
+ if trigger_info.total_number_of_exposures == 0
59
+ else trigger_info.total_number_of_exposures
57
60
  ),
58
61
  self.driver.image_mode.set(adcore.ADImageMode.MULTIPLE),
59
62
  )
@@ -70,13 +73,3 @@ class PilatusController(adcore.ADBaseController[PilatusDriverIO]):
70
73
  True,
71
74
  timeout=DEFAULT_TIMEOUT,
72
75
  )
73
-
74
- @classmethod
75
- def _get_trigger_mode(cls, trigger: DetectorTrigger) -> PilatusTriggerMode:
76
- if trigger not in cls._supported_trigger_types.keys():
77
- raise ValueError(
78
- f"{cls.__name__} only supports the following trigger "
79
- f"types: {cls._supported_trigger_types.keys()} but was asked to "
80
- f"use {trigger}"
81
- )
82
- return cls._supported_trigger_types[trigger]
@@ -21,4 +21,4 @@ class PilatusDriverIO(adcore.ADBaseIO):
21
21
  """This mirrors the interface provided by ADPilatus/db/pilatus.template."""
22
22
  """See HTML docs at https://areadetector.github.io/areaDetector/ADPilatus/pilatusDoc.html"""
23
23
  trigger_mode: A[SignalRW[PilatusTriggerMode], PvSuffix.rbv("TriggerMode")]
24
- armed: A[SignalR[bool], PvSuffix.rbv("Armed_RBV")]
24
+ armed: A[SignalR[bool], PvSuffix("Armed")]
@@ -40,7 +40,7 @@ class VimbaController(adcore.ADBaseController[VimbaDriverIO]):
40
40
  await asyncio.gather(
41
41
  self.driver.trigger_mode.set(TRIGGER_MODE[trigger_info.trigger]),
42
42
  self.driver.exposure_mode.set(EXPOSE_OUT_MODE[trigger_info.trigger]),
43
- self.driver.num_images.set(trigger_info.total_number_of_triggers),
43
+ self.driver.num_images.set(trigger_info.total_number_of_exposures),
44
44
  self.driver.image_mode.set(adcore.ADImageMode.MULTIPLE),
45
45
  )
46
46
  if trigger_info.livetime is not None and trigger_info.trigger not in [
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  import sys
3
3
  import typing
4
- from collections.abc import Sequence
4
+ from collections.abc import Mapping, Sequence
5
5
  from functools import cache
6
6
  from math import isnan, nan
7
7
  from typing import Any, Generic, cast
@@ -146,7 +146,7 @@ class CaBoolConverter(CaConverter[bool]):
146
146
 
147
147
 
148
148
  class CaEnumConverter(CaConverter[str]):
149
- def __init__(self, supported_values: dict[str, str]):
149
+ def __init__(self, supported_values: Mapping[str, str]):
150
150
  self.supported_values = supported_values
151
151
  super().__init__(
152
152
  str, dbr.DBR_STRING, metadata=SignalMetadata(choices=list(supported_values))
@@ -383,7 +383,7 @@ class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
383
383
 
384
384
  async def put(self, value: SignalDatatypeT | None, wait: bool):
385
385
  if value is None:
386
- write_value = self.initial_values[self.write_pv]
386
+ write_value = self.initial_values[self.write_pv]["value"]
387
387
  else:
388
388
  write_value = self.converter.write_value(value)
389
389
  await context().put(self.write_pv, {"value": write_value}, wait=wait)
@@ -43,10 +43,10 @@ FastCSPVIVector = dict[Literal["d"], Entry]
43
43
 
44
44
  def _get_signal_details(entry: Entry) -> tuple[type[Signal], str, str]:
45
45
  match entry:
46
- case {"r": read_pv}:
47
- return SignalR, read_pv, read_pv
48
46
  case {"r": read_pv, "w": write_pv}:
49
47
  return SignalRW, read_pv, write_pv
48
+ case {"r": read_pv}:
49
+ return SignalR, read_pv, read_pv
50
50
  case {"w": write_pv}:
51
51
  return SignalW, write_pv, write_pv
52
52
  case {"rw": read_write_pv}:
@@ -77,6 +77,8 @@ class PviDeviceConnector(DeviceConnector):
77
77
  hinted Signals are not present.
78
78
  """
79
79
 
80
+ mock_device_vector_len: int = 2
81
+
80
82
  def __init__(self, prefix: str = "", error_hint: str = "") -> None:
81
83
  # TODO: what happens if we get a leading "pva://" here?
82
84
  self.prefix = prefix
@@ -110,7 +112,7 @@ class PviDeviceConnector(DeviceConnector):
110
112
  backend.write_pv = write_pv
111
113
 
112
114
  async def connect_mock(self, device: Device, mock: LazyMock):
113
- self.filler.create_device_vector_entries_to_mock(2)
115
+ self.filler.create_device_vector_entries_to_mock(self.mock_device_vector_len)
114
116
  # Set the name of the device to name all children
115
117
  device.set_name(device.name)
116
118
  return await super().connect_mock(device, mock)