ophyd-async 0.5.1__py3-none-any.whl → 0.6.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 (73) hide show
  1. ophyd_async/__init__.py +10 -1
  2. ophyd_async/__main__.py +12 -4
  3. ophyd_async/_version.py +2 -2
  4. ophyd_async/core/__init__.py +15 -6
  5. ophyd_async/core/_detector.py +72 -63
  6. ophyd_async/core/_device.py +13 -15
  7. ophyd_async/core/_device_save_loader.py +30 -19
  8. ophyd_async/core/_flyer.py +6 -4
  9. ophyd_async/core/_hdf_dataset.py +8 -9
  10. ophyd_async/core/_log.py +3 -1
  11. ophyd_async/core/_mock_signal_backend.py +11 -9
  12. ophyd_async/core/_mock_signal_utils.py +8 -5
  13. ophyd_async/core/_protocol.py +7 -7
  14. ophyd_async/core/_providers.py +17 -13
  15. ophyd_async/core/_readable.py +30 -22
  16. ophyd_async/core/_signal.py +53 -52
  17. ophyd_async/core/_signal_backend.py +20 -7
  18. ophyd_async/core/_soft_signal_backend.py +62 -32
  19. ophyd_async/core/_status.py +18 -4
  20. ophyd_async/core/_table.py +63 -0
  21. ophyd_async/core/_utils.py +24 -28
  22. ophyd_async/epics/adaravis/_aravis.py +1 -1
  23. ophyd_async/epics/adaravis/_aravis_controller.py +17 -16
  24. ophyd_async/epics/adaravis/_aravis_io.py +2 -1
  25. ophyd_async/epics/adcore/__init__.py +2 -2
  26. ophyd_async/epics/adcore/_core_io.py +2 -0
  27. ophyd_async/epics/adcore/_core_logic.py +9 -7
  28. ophyd_async/epics/adcore/_hdf_writer.py +26 -21
  29. ophyd_async/epics/adcore/_single_trigger.py +1 -1
  30. ophyd_async/epics/adcore/_utils.py +5 -6
  31. ophyd_async/epics/adkinetix/_kinetix.py +1 -1
  32. ophyd_async/epics/adkinetix/_kinetix_controller.py +19 -14
  33. ophyd_async/epics/adpilatus/_pilatus.py +1 -1
  34. ophyd_async/epics/adpilatus/_pilatus_controller.py +18 -16
  35. ophyd_async/epics/adsimdetector/_sim.py +7 -6
  36. ophyd_async/epics/adsimdetector/_sim_controller.py +20 -15
  37. ophyd_async/epics/advimba/_vimba.py +1 -1
  38. ophyd_async/epics/advimba/_vimba_controller.py +21 -16
  39. ophyd_async/epics/demo/_mover.py +4 -5
  40. ophyd_async/epics/demo/sensor.db +0 -1
  41. ophyd_async/epics/eiger/__init__.py +5 -0
  42. ophyd_async/epics/eiger/_eiger.py +43 -0
  43. ophyd_async/epics/eiger/_eiger_controller.py +66 -0
  44. ophyd_async/epics/eiger/_eiger_io.py +42 -0
  45. ophyd_async/epics/eiger/_odin_io.py +126 -0
  46. ophyd_async/epics/motor.py +9 -11
  47. ophyd_async/epics/pvi/_pvi.py +30 -33
  48. ophyd_async/epics/signal/_aioca.py +55 -25
  49. ophyd_async/epics/signal/_common.py +3 -10
  50. ophyd_async/epics/signal/_epics_transport.py +11 -8
  51. ophyd_async/epics/signal/_p4p.py +79 -30
  52. ophyd_async/epics/signal/_signal.py +6 -8
  53. ophyd_async/fastcs/panda/__init__.py +0 -6
  54. ophyd_async/fastcs/panda/_control.py +14 -15
  55. ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
  56. ophyd_async/fastcs/panda/_table.py +111 -138
  57. ophyd_async/fastcs/panda/_trigger.py +1 -2
  58. ophyd_async/fastcs/panda/_utils.py +3 -2
  59. ophyd_async/fastcs/panda/_writer.py +28 -13
  60. ophyd_async/plan_stubs/_fly.py +16 -16
  61. ophyd_async/plan_stubs/_nd_attributes.py +12 -6
  62. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
  63. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +24 -20
  64. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
  65. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
  66. ophyd_async/sim/demo/_sim_motor.py +2 -1
  67. {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/METADATA +46 -45
  68. ophyd_async-0.6.0.dist-info/RECORD +96 -0
  69. {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/WHEEL +1 -1
  70. ophyd_async-0.5.1.dist-info/RECORD +0 -90
  71. {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/LICENSE +0 -0
  72. {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/entry_points.txt +0 -0
  73. {ophyd_async-0.5.1.dist-info → ophyd_async-0.6.0.dist-info}/top_level.txt +0 -0
@@ -2,23 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
+ from collections.abc import Awaitable, Callable, Iterable
5
6
  from dataclasses import dataclass
6
- from typing import (
7
- Awaitable,
8
- Callable,
9
- Dict,
10
- Generic,
11
- Iterable,
12
- List,
13
- Optional,
14
- ParamSpec,
15
- Type,
16
- TypeVar,
17
- Union,
18
- )
7
+ from typing import Generic, Literal, ParamSpec, TypeVar, get_origin
19
8
 
20
9
  import numpy as np
21
10
  from bluesky.protocols import Reading
11
+ from pydantic import BaseModel
22
12
 
23
13
  T = TypeVar("T")
24
14
  P = ParamSpec("P")
@@ -28,18 +18,18 @@ Callback = Callable[[T], None]
28
18
  #: monitor updates
29
19
  ReadingValueCallback = Callable[[Reading, T], None]
30
20
  DEFAULT_TIMEOUT = 10.0
31
- ErrorText = Union[str, Dict[str, Exception]]
21
+ ErrorText = str | dict[str, Exception]
32
22
 
33
23
 
34
- class CalculateTimeout:
35
- """Sentinel class used to implement ``myfunc(timeout=CalculateTimeout)``
24
+ CALCULATE_TIMEOUT = "CALCULATE_TIMEOUT"
25
+ """Sentinel used to implement ``myfunc(timeout=CalculateTimeout)``
36
26
 
37
- This signifies that the function should calculate a suitable non-zero
38
- timeout itself
39
- """
27
+ This signifies that the function should calculate a suitable non-zero
28
+ timeout itself
29
+ """
40
30
 
41
31
 
42
- CalculatableTimeout = float | None | Type[CalculateTimeout]
32
+ CalculatableTimeout = float | None | Literal["CALCULATE_TIMEOUT"]
43
33
 
44
34
 
45
35
  class NotConnected(Exception):
@@ -115,7 +105,7 @@ async def wait_for_connection(**coros: Awaitable[None]):
115
105
  results = await asyncio.gather(*coros.values(), return_exceptions=True)
116
106
  exceptions = {}
117
107
 
118
- for name, result in zip(coros, results):
108
+ for name, result in zip(coros, results, strict=False):
119
109
  if isinstance(result, Exception):
120
110
  exceptions[name] = result
121
111
  if not isinstance(result, NotConnected):
@@ -129,7 +119,7 @@ async def wait_for_connection(**coros: Awaitable[None]):
129
119
  raise NotConnected(exceptions)
130
120
 
131
121
 
132
- def get_dtype(typ: Type) -> Optional[np.dtype]:
122
+ def get_dtype(typ: type) -> np.dtype | None:
133
123
  """Get the runtime dtype from a numpy ndarray type annotation
134
124
 
135
125
  >>> import numpy.typing as npt
@@ -144,8 +134,8 @@ def get_dtype(typ: Type) -> Optional[np.dtype]:
144
134
  return None
145
135
 
146
136
 
147
- def get_unique(values: Dict[str, T], types: str) -> T:
148
- """If all values are the same, return that value, otherwise return TypeError
137
+ def get_unique(values: dict[str, T], types: str) -> T:
138
+ """If all values are the same, return that value, otherwise raise TypeError
149
139
 
150
140
  >>> get_unique({"a": 1, "b": 1}, "integers")
151
141
  1
@@ -162,21 +152,21 @@ def get_unique(values: Dict[str, T], types: str) -> T:
162
152
 
163
153
 
164
154
  async def merge_gathered_dicts(
165
- coros: Iterable[Awaitable[Dict[str, T]]],
166
- ) -> Dict[str, T]:
155
+ coros: Iterable[Awaitable[dict[str, T]]],
156
+ ) -> dict[str, T]:
167
157
  """Merge dictionaries produced by a sequence of coroutines.
168
158
 
169
159
  Can be used for merging ``read()`` or ``describe``. For instance::
170
160
 
171
161
  combined_read = await merge_gathered_dicts(s.read() for s in signals)
172
162
  """
173
- ret: Dict[str, T] = {}
163
+ ret: dict[str, T] = {}
174
164
  for result in await asyncio.gather(*coros):
175
165
  ret.update(result)
176
166
  return ret
177
167
 
178
168
 
179
- async def gather_list(coros: Iterable[Awaitable[T]]) -> List[T]:
169
+ async def gather_list(coros: Iterable[Awaitable[T]]) -> list[T]:
180
170
  return await asyncio.gather(*coros)
181
171
 
182
172
 
@@ -195,3 +185,9 @@ def in_micros(t: float) -> int:
195
185
  if t < 0:
196
186
  raise ValueError(f"Expected a positive time in seconds, got {t!r}")
197
187
  return int(np.ceil(t * 1e6))
188
+
189
+
190
+ def is_pydantic_model(datatype) -> bool:
191
+ while origin := get_origin(datatype):
192
+ datatype = origin
193
+ return datatype and issubclass(datatype, BaseModel)
@@ -37,7 +37,7 @@ class AravisDetector(StandardDetector, HasHints):
37
37
  self.hdf,
38
38
  path_provider,
39
39
  lambda: self.name,
40
- adcore.ADBaseShapeProvider(self.drv),
40
+ adcore.ADBaseDatasetDescriber(self.drv),
41
41
  ),
42
42
  config_sigs=(self.drv.acquire_time,),
43
43
  name=name,
@@ -1,12 +1,13 @@
1
1
  import asyncio
2
- from typing import Literal, Optional, Tuple
2
+ from typing import Literal
3
3
 
4
4
  from ophyd_async.core import (
5
- AsyncStatus,
6
5
  DetectorControl,
7
6
  DetectorTrigger,
7
+ TriggerInfo,
8
8
  set_and_wait_for_value,
9
9
  )
10
+ from ophyd_async.core._status import AsyncStatus
10
11
  from ophyd_async.epics import adcore
11
12
 
12
13
  from ._aravis_io import AravisDriverIO, AravisTriggerMode, AravisTriggerSource
@@ -23,24 +24,20 @@ class AravisController(DetectorControl):
23
24
  def __init__(self, driver: AravisDriverIO, gpio_number: GPIO_NUMBER) -> None:
24
25
  self._drv = driver
25
26
  self.gpio_number = gpio_number
27
+ self._arm_status: AsyncStatus | None = None
26
28
 
27
- def get_deadtime(self, exposure: float) -> float:
29
+ def get_deadtime(self, exposure: float | None) -> float:
28
30
  return _HIGHEST_POSSIBLE_DEADTIME
29
31
 
30
- async def arm(
31
- self,
32
- num: int = 0,
33
- trigger: DetectorTrigger = DetectorTrigger.internal,
34
- exposure: Optional[float] = None,
35
- ) -> AsyncStatus:
36
- if num == 0:
32
+ async def prepare(self, trigger_info: TriggerInfo):
33
+ if (num := trigger_info.number) == 0:
37
34
  image_mode = adcore.ImageMode.continuous
38
35
  else:
39
36
  image_mode = adcore.ImageMode.multiple
40
- if exposure is not None:
37
+ if (exposure := trigger_info.livetime) is not None:
41
38
  await self._drv.acquire_time.set(exposure)
42
39
 
43
- trigger_mode, trigger_source = self._get_trigger_info(trigger)
40
+ trigger_mode, trigger_source = self._get_trigger_info(trigger_info.trigger)
44
41
  # trigger mode must be set first and on it's own!
45
42
  await self._drv.trigger_mode.set(trigger_mode)
46
43
 
@@ -50,12 +47,16 @@ class AravisController(DetectorControl):
50
47
  self._drv.image_mode.set(image_mode),
51
48
  )
52
49
 
53
- status = await set_and_wait_for_value(self._drv.acquire, True)
54
- return status
50
+ async def arm(self):
51
+ self._arm_status = await set_and_wait_for_value(self._drv.acquire, True)
52
+
53
+ async def wait_for_idle(self):
54
+ if self._arm_status:
55
+ await self._arm_status
55
56
 
56
57
  def _get_trigger_info(
57
58
  self, trigger: DetectorTrigger
58
- ) -> Tuple[AravisTriggerMode, AravisTriggerSource]:
59
+ ) -> tuple[AravisTriggerMode, AravisTriggerSource]:
59
60
  supported_trigger_types = (
60
61
  DetectorTrigger.constant_gate,
61
62
  DetectorTrigger.edge_trigger,
@@ -70,7 +71,7 @@ class AravisController(DetectorControl):
70
71
  if trigger == DetectorTrigger.internal:
71
72
  return AravisTriggerMode.off, "Freerun"
72
73
  else:
73
- return (AravisTriggerMode.on, f"Line{self.gpio_number}")
74
+ return (AravisTriggerMode.on, f"Line{self.gpio_number}") # type: ignore
74
75
 
75
76
  async def disarm(self):
76
77
  await adcore.stop_busy_record(self._drv.acquire, False, timeout=1)
@@ -38,6 +38,7 @@ class AravisDriverIO(adcore.ADBaseIO):
38
38
  AravisTriggerMode, prefix + "TriggerMode"
39
39
  )
40
40
  self.trigger_source = epics_signal_rw_rbv(
41
- AravisTriggerSource, prefix + "TriggerSource"
41
+ AravisTriggerSource, # type: ignore
42
+ prefix + "TriggerSource",
42
43
  )
43
44
  super().__init__(prefix, name=name)
@@ -7,7 +7,7 @@ from ._core_io import (
7
7
  )
8
8
  from ._core_logic import (
9
9
  DEFAULT_GOOD_STATES,
10
- ADBaseShapeProvider,
10
+ ADBaseDatasetDescriber,
11
11
  set_exposure_time_and_acquire_period_if_supplied,
12
12
  start_acquiring_driver_and_ensure_status,
13
13
  )
@@ -31,7 +31,7 @@ __all__ = [
31
31
  "NDFileHDFIO",
32
32
  "NDPluginStatsIO",
33
33
  "DEFAULT_GOOD_STATES",
34
- "ADBaseShapeProvider",
34
+ "ADBaseDatasetDescriber",
35
35
  "set_exposure_time_and_acquire_period_if_supplied",
36
36
  "start_acquiring_driver_and_ensure_status",
37
37
  "ADHDFWriter",
@@ -135,4 +135,6 @@ class NDFileHDFIO(NDPluginBaseIO):
135
135
  self.array_size0 = epics_signal_r(int, prefix + "ArraySize0")
136
136
  self.array_size1 = epics_signal_r(int, prefix + "ArraySize1")
137
137
  self.create_directory = epics_signal_rw(int, prefix + "CreateDirectory")
138
+ self.num_frames_chunks = epics_signal_r(int, prefix + "NumFramesChunks_RBV")
139
+ self.chunk_size_auto = epics_signal_rw_rbv(bool, prefix + "ChunkSizeAuto")
138
140
  super().__init__(prefix, name)
@@ -1,32 +1,34 @@
1
1
  import asyncio
2
- from typing import FrozenSet, Set
3
2
 
4
3
  from ophyd_async.core import (
5
4
  DEFAULT_TIMEOUT,
6
5
  AsyncStatus,
6
+ DatasetDescriber,
7
7
  DetectorControl,
8
- ShapeProvider,
9
8
  set_and_wait_for_value,
10
9
  )
10
+ from ophyd_async.epics.adcore._utils import convert_ad_dtype_to_np
11
11
 
12
12
  from ._core_io import ADBaseIO, DetectorState
13
13
 
14
14
  # Default set of states that we should consider "good" i.e. the acquisition
15
15
  # is complete and went well
16
- DEFAULT_GOOD_STATES: FrozenSet[DetectorState] = frozenset(
16
+ DEFAULT_GOOD_STATES: frozenset[DetectorState] = frozenset(
17
17
  [DetectorState.Idle, DetectorState.Aborted]
18
18
  )
19
19
 
20
20
 
21
- class ADBaseShapeProvider(ShapeProvider):
21
+ class ADBaseDatasetDescriber(DatasetDescriber):
22
22
  def __init__(self, driver: ADBaseIO) -> None:
23
23
  self._driver = driver
24
24
 
25
- async def __call__(self) -> tuple:
25
+ async def np_datatype(self) -> str:
26
+ return convert_ad_dtype_to_np(await self._driver.data_type.get_value())
27
+
28
+ async def shape(self) -> tuple[int, int]:
26
29
  shape = await asyncio.gather(
27
30
  self._driver.array_size_y.get_value(),
28
31
  self._driver.array_size_x.get_value(),
29
- self._driver.data_type.get_value(),
30
32
  )
31
33
  return shape
32
34
 
@@ -63,7 +65,7 @@ async def set_exposure_time_and_acquire_period_if_supplied(
63
65
 
64
66
  async def start_acquiring_driver_and_ensure_status(
65
67
  driver: ADBaseIO,
66
- good_states: Set[DetectorState] = set(DEFAULT_GOOD_STATES),
68
+ good_states: frozenset[DetectorState] = frozenset(DEFAULT_GOOD_STATES),
67
69
  timeout: float = DEFAULT_TIMEOUT,
68
70
  ) -> AsyncStatus:
69
71
  """
@@ -1,19 +1,20 @@
1
1
  import asyncio
2
+ from collections.abc import AsyncGenerator, AsyncIterator
2
3
  from pathlib import Path
3
- from typing import AsyncGenerator, AsyncIterator, Dict, List, Optional
4
4
  from xml.etree import ElementTree as ET
5
5
 
6
- from bluesky.protocols import DataKey, Hints, StreamAsset
6
+ from bluesky.protocols import Hints, StreamAsset
7
+ from event_model import DataKey
7
8
 
8
9
  from ophyd_async.core import (
9
10
  DEFAULT_TIMEOUT,
10
11
  AsyncStatus,
12
+ DatasetDescriber,
11
13
  DetectorWriter,
12
14
  HDFDataset,
13
15
  HDFFile,
14
16
  NameProvider,
15
17
  PathProvider,
16
- ShapeProvider,
17
18
  observe_value,
18
19
  set_and_wait_for_value,
19
20
  wait_for_value,
@@ -22,7 +23,6 @@ from ophyd_async.core import (
22
23
  from ._core_io import NDArrayBaseIO, NDFileHDFIO
23
24
  from ._utils import (
24
25
  FileWriteMode,
25
- convert_ad_dtype_to_np,
26
26
  convert_param_dtype_to_np,
27
27
  convert_pv_dtype_to_np,
28
28
  )
@@ -34,28 +34,31 @@ class ADHDFWriter(DetectorWriter):
34
34
  hdf: NDFileHDFIO,
35
35
  path_provider: PathProvider,
36
36
  name_provider: NameProvider,
37
- shape_provider: ShapeProvider,
37
+ dataset_describer: DatasetDescriber,
38
38
  *plugins: NDArrayBaseIO,
39
39
  ) -> None:
40
40
  self.hdf = hdf
41
41
  self._path_provider = path_provider
42
42
  self._name_provider = name_provider
43
- self._shape_provider = shape_provider
43
+ self._dataset_describer = dataset_describer
44
44
 
45
45
  self._plugins = plugins
46
- self._capture_status: Optional[AsyncStatus] = None
47
- self._datasets: List[HDFDataset] = []
48
- self._file: Optional[HDFFile] = None
46
+ self._capture_status: AsyncStatus | None = None
47
+ self._datasets: list[HDFDataset] = []
48
+ self._file: HDFFile | None = None
49
49
  self._multiplier = 1
50
50
 
51
- async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
51
+ async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
52
52
  self._file = None
53
- info = self._path_provider(device_name=self.hdf.name)
53
+ info = self._path_provider(device_name=self._name_provider())
54
54
 
55
55
  # Set the directory creation depth first, since dir creation callback happens
56
56
  # when directory path PV is processed.
57
57
  await self.hdf.create_directory.set(info.create_dir_depth)
58
58
 
59
+ # Make sure we are using chunk auto-sizing
60
+ await asyncio.gather(self.hdf.chunk_size_auto.set(True))
61
+
59
62
  await asyncio.gather(
60
63
  self.hdf.num_extra_dims.set(0),
61
64
  self.hdf.lazy_open.set(True),
@@ -79,24 +82,23 @@ class ADHDFWriter(DetectorWriter):
79
82
  # Wait for it to start, stashing the status that tells us when it finishes
80
83
  self._capture_status = await set_and_wait_for_value(self.hdf.capture, True)
81
84
  name = self._name_provider()
82
- detector_shape = tuple(await self._shape_provider())
85
+ detector_shape = await self._dataset_describer.shape()
86
+ np_dtype = await self._dataset_describer.np_datatype()
83
87
  self._multiplier = multiplier
84
88
  outer_shape = (multiplier,) if multiplier > 1 else ()
85
- frame_shape = detector_shape[:-1] if len(detector_shape) > 0 else []
86
- dtype_numpy = (
87
- convert_ad_dtype_to_np(detector_shape[-1])
88
- if len(detector_shape) > 0
89
- else ""
90
- )
89
+
90
+ # Determine number of frames that will be saved per HDF chunk
91
+ frames_per_chunk = await self.hdf.num_frames_chunks.get_value()
91
92
 
92
93
  # Add the main data
93
94
  self._datasets = [
94
95
  HDFDataset(
95
96
  data_key=name,
96
97
  dataset="/entry/data/data",
97
- shape=frame_shape,
98
- dtype_numpy=dtype_numpy,
98
+ shape=detector_shape,
99
+ dtype_numpy=np_dtype,
99
100
  multiplier=multiplier,
101
+ chunk_shape=(frames_per_chunk, *detector_shape),
100
102
  )
101
103
  ]
102
104
  # And all the scalar datasets
@@ -123,6 +125,9 @@ class ADHDFWriter(DetectorWriter):
123
125
  (),
124
126
  np_datatype,
125
127
  multiplier,
128
+ # NDAttributes appear to always be configured with
129
+ # this chunk size
130
+ chunk_shape=(16384,),
126
131
  )
127
132
  )
128
133
 
@@ -131,7 +136,7 @@ class ADHDFWriter(DetectorWriter):
131
136
  source=self.hdf.full_file_name.source,
132
137
  shape=outer_shape + tuple(ds.shape),
133
138
  dtype="array" if ds.shape else "number",
134
- dtype_numpy=ds.dtype_numpy,
139
+ dtype_numpy=ds.dtype_numpy, # type: ignore
135
140
  external="STREAM:",
136
141
  )
137
142
  for ds in self._datasets
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from typing import Sequence
2
+ from collections.abc import Sequence
3
3
 
4
4
  from bluesky.protocols import Triggerable
5
5
 
@@ -1,6 +1,5 @@
1
1
  from dataclasses import dataclass
2
2
  from enum import Enum
3
- from typing import Optional
4
3
 
5
4
  from ophyd_async.core import DEFAULT_TIMEOUT, SignalRW, T, wait_for_value
6
5
  from ophyd_async.core._signal import SignalR
@@ -51,8 +50,8 @@ def convert_pv_dtype_to_np(datatype: str) -> str:
51
50
  else:
52
51
  try:
53
52
  np_datatype = convert_ad_dtype_to_np(_pvattribute_to_ad_datatype[datatype])
54
- except KeyError:
55
- raise ValueError(f"Invalid dbr type {datatype}")
53
+ except KeyError as e:
54
+ raise ValueError(f"Invalid dbr type {datatype}") from e
56
55
  return np_datatype
57
56
 
58
57
 
@@ -69,8 +68,8 @@ def convert_param_dtype_to_np(datatype: str) -> str:
69
68
  np_datatype = convert_ad_dtype_to_np(
70
69
  _paramattribute_to_ad_datatype[datatype]
71
70
  )
72
- except KeyError:
73
- raise ValueError(f"Invalid datatype {datatype}")
71
+ except KeyError as e:
72
+ raise ValueError(f"Invalid datatype {datatype}") from e
74
73
  return np_datatype
75
74
 
76
75
 
@@ -126,7 +125,7 @@ async def stop_busy_record(
126
125
  signal: SignalRW[T],
127
126
  value: T,
128
127
  timeout: float = DEFAULT_TIMEOUT,
129
- status_timeout: Optional[float] = None,
128
+ status_timeout: float | None = None,
130
129
  ) -> None:
131
130
  await signal.set(value, wait=False, timeout=status_timeout)
132
131
  await wait_for_value(signal, value, timeout=timeout)
@@ -33,7 +33,7 @@ class KinetixDetector(StandardDetector, HasHints):
33
33
  self.hdf,
34
34
  path_provider,
35
35
  lambda: self.name,
36
- adcore.ADBaseShapeProvider(self.drv),
36
+ adcore.ADBaseDatasetDescriber(self.drv),
37
37
  ),
38
38
  config_sigs=(self.drv.acquire_time,),
39
39
  name=name,
@@ -1,7 +1,8 @@
1
1
  import asyncio
2
- from typing import Optional
3
2
 
4
- from ophyd_async.core import AsyncStatus, DetectorControl, DetectorTrigger
3
+ from ophyd_async.core import DetectorControl, DetectorTrigger
4
+ from ophyd_async.core._detector import TriggerInfo
5
+ from ophyd_async.core._status import AsyncStatus
5
6
  from ophyd_async.epics import adcore
6
7
 
7
8
  from ._kinetix_io import KinetixDriverIO, KinetixTriggerMode
@@ -20,27 +21,31 @@ class KinetixController(DetectorControl):
20
21
  driver: KinetixDriverIO,
21
22
  ) -> None:
22
23
  self._drv = driver
24
+ self._arm_status: AsyncStatus | None = None
23
25
 
24
- def get_deadtime(self, exposure: float) -> float:
26
+ def get_deadtime(self, exposure: float | None) -> float:
25
27
  return 0.001
26
28
 
27
- async def arm(
28
- self,
29
- num: int,
30
- trigger: DetectorTrigger = DetectorTrigger.internal,
31
- exposure: Optional[float] = None,
32
- ) -> AsyncStatus:
29
+ async def prepare(self, trigger_info: TriggerInfo):
33
30
  await asyncio.gather(
34
- self._drv.trigger_mode.set(KINETIX_TRIGGER_MODE_MAP[trigger]),
35
- self._drv.num_images.set(num),
31
+ self._drv.trigger_mode.set(KINETIX_TRIGGER_MODE_MAP[trigger_info.trigger]),
32
+ self._drv.num_images.set(trigger_info.number),
36
33
  self._drv.image_mode.set(adcore.ImageMode.multiple),
37
34
  )
38
- if exposure is not None and trigger not in [
35
+ if trigger_info.livetime is not None and trigger_info.trigger not in [
39
36
  DetectorTrigger.variable_gate,
40
37
  DetectorTrigger.constant_gate,
41
38
  ]:
42
- await self._drv.acquire_time.set(exposure)
43
- return await adcore.start_acquiring_driver_and_ensure_status(self._drv)
39
+ await self._drv.acquire_time.set(trigger_info.livetime)
40
+
41
+ async def arm(self):
42
+ self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
43
+ self._drv
44
+ )
45
+
46
+ async def wait_for_idle(self):
47
+ if self._arm_status:
48
+ await self._arm_status
44
49
 
45
50
  async def disarm(self):
46
51
  await adcore.stop_busy_record(self._drv.acquire, False, timeout=1)
@@ -47,7 +47,7 @@ class PilatusDetector(StandardDetector):
47
47
  self.hdf,
48
48
  path_provider,
49
49
  lambda: self.name,
50
- adcore.ADBaseShapeProvider(self.drv),
50
+ adcore.ADBaseDatasetDescriber(self.drv),
51
51
  ),
52
52
  config_sigs=(self.drv.acquire_time,),
53
53
  name=name,
@@ -1,13 +1,13 @@
1
1
  import asyncio
2
- from typing import Optional
3
2
 
4
3
  from ophyd_async.core import (
5
4
  DEFAULT_TIMEOUT,
6
- AsyncStatus,
7
5
  DetectorControl,
8
6
  DetectorTrigger,
9
7
  wait_for_value,
10
8
  )
9
+ from ophyd_async.core._detector import TriggerInfo
10
+ from ophyd_async.core._status import AsyncStatus
11
11
  from ophyd_async.epics import adcore
12
12
 
13
13
  from ._pilatus_io import PilatusDriverIO, PilatusTriggerMode
@@ -27,29 +27,29 @@ class PilatusController(DetectorControl):
27
27
  ) -> None:
28
28
  self._drv = driver
29
29
  self._readout_time = readout_time
30
+ self._arm_status: AsyncStatus | None = None
30
31
 
31
- def get_deadtime(self, exposure: float) -> float:
32
+ def get_deadtime(self, exposure: float | None) -> float:
32
33
  return self._readout_time
33
34
 
34
- async def arm(
35
- self,
36
- num: int,
37
- trigger: DetectorTrigger = DetectorTrigger.internal,
38
- exposure: Optional[float] = None,
39
- ) -> AsyncStatus:
40
- if exposure is not None:
35
+ async def prepare(self, trigger_info: TriggerInfo):
36
+ if trigger_info.livetime is not None:
41
37
  await adcore.set_exposure_time_and_acquire_period_if_supplied(
42
- self, self._drv, exposure
38
+ self, self._drv, trigger_info.livetime
43
39
  )
44
40
  await asyncio.gather(
45
- self._drv.trigger_mode.set(self._get_trigger_mode(trigger)),
46
- self._drv.num_images.set(999_999 if num == 0 else num),
41
+ self._drv.trigger_mode.set(self._get_trigger_mode(trigger_info.trigger)),
42
+ self._drv.num_images.set(
43
+ 999_999 if trigger_info.number == 0 else trigger_info.number
44
+ ),
47
45
  self._drv.image_mode.set(adcore.ImageMode.multiple),
48
46
  )
49
47
 
48
+ async def arm(self):
50
49
  # Standard arm the detector and wait for the acquire PV to be True
51
- idle_status = await adcore.start_acquiring_driver_and_ensure_status(self._drv)
52
-
50
+ self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
51
+ self._drv
52
+ )
53
53
  # The pilatus has an additional PV that goes True when the camserver
54
54
  # is actually ready. Should wait for that too or we risk dropping
55
55
  # a frame
@@ -59,7 +59,9 @@ class PilatusController(DetectorControl):
59
59
  timeout=DEFAULT_TIMEOUT,
60
60
  )
61
61
 
62
- return idle_status
62
+ async def wait_for_idle(self):
63
+ if self._arm_status:
64
+ await self._arm_status
63
65
 
64
66
  @classmethod
65
67
  def _get_trigger_mode(cls, trigger: DetectorTrigger) -> PilatusTriggerMode:
@@ -1,4 +1,4 @@
1
- from typing import Sequence
1
+ from collections.abc import Sequence
2
2
 
3
3
  from ophyd_async.core import PathProvider, SignalR, StandardDetector
4
4
  from ophyd_async.epics import adcore
@@ -12,14 +12,15 @@ class SimDetector(StandardDetector):
12
12
 
13
13
  def __init__(
14
14
  self,
15
- drv: adcore.ADBaseIO,
16
- hdf: adcore.NDFileHDFIO,
15
+ prefix: str,
17
16
  path_provider: PathProvider,
17
+ drv_suffix="cam1:",
18
+ hdf_suffix="HDF1:",
18
19
  name: str = "",
19
20
  config_sigs: Sequence[SignalR] = (),
20
21
  ):
21
- self.drv = drv
22
- self.hdf = hdf
22
+ self.drv = adcore.ADBaseIO(prefix + drv_suffix)
23
+ self.hdf = adcore.NDFileHDFIO(prefix + hdf_suffix)
23
24
 
24
25
  super().__init__(
25
26
  SimController(self.drv),
@@ -27,7 +28,7 @@ class SimDetector(StandardDetector):
27
28
  self.hdf,
28
29
  path_provider,
29
30
  lambda: self.name,
30
- adcore.ADBaseShapeProvider(self.drv),
31
+ adcore.ADBaseDatasetDescriber(self.drv),
31
32
  ),
32
33
  config_sigs=config_sigs,
33
34
  name=name,