ophyd-async 0.5.2__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 (66) 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 +11 -3
  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 +11 -11
  15. ophyd_async/core/_readable.py +30 -22
  16. ophyd_async/core/_signal.py +52 -51
  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 +7 -9
  20. ophyd_async/core/_table.py +63 -0
  21. ophyd_async/core/_utils.py +24 -28
  22. ophyd_async/epics/adaravis/_aravis_controller.py +17 -16
  23. ophyd_async/epics/adaravis/_aravis_io.py +2 -1
  24. ophyd_async/epics/adcore/_core_io.py +2 -0
  25. ophyd_async/epics/adcore/_core_logic.py +2 -3
  26. ophyd_async/epics/adcore/_hdf_writer.py +19 -8
  27. ophyd_async/epics/adcore/_single_trigger.py +1 -1
  28. ophyd_async/epics/adcore/_utils.py +5 -6
  29. ophyd_async/epics/adkinetix/_kinetix_controller.py +19 -14
  30. ophyd_async/epics/adpilatus/_pilatus_controller.py +18 -16
  31. ophyd_async/epics/adsimdetector/_sim.py +6 -5
  32. ophyd_async/epics/adsimdetector/_sim_controller.py +20 -15
  33. ophyd_async/epics/advimba/_vimba_controller.py +21 -16
  34. ophyd_async/epics/demo/_mover.py +4 -5
  35. ophyd_async/epics/demo/sensor.db +0 -1
  36. ophyd_async/epics/eiger/_eiger.py +1 -1
  37. ophyd_async/epics/eiger/_eiger_controller.py +16 -16
  38. ophyd_async/epics/eiger/_odin_io.py +6 -5
  39. ophyd_async/epics/motor.py +8 -10
  40. ophyd_async/epics/pvi/_pvi.py +30 -33
  41. ophyd_async/epics/signal/_aioca.py +55 -25
  42. ophyd_async/epics/signal/_common.py +3 -10
  43. ophyd_async/epics/signal/_epics_transport.py +11 -8
  44. ophyd_async/epics/signal/_p4p.py +79 -30
  45. ophyd_async/epics/signal/_signal.py +6 -8
  46. ophyd_async/fastcs/panda/__init__.py +0 -6
  47. ophyd_async/fastcs/panda/_control.py +14 -15
  48. ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
  49. ophyd_async/fastcs/panda/_table.py +111 -138
  50. ophyd_async/fastcs/panda/_trigger.py +1 -2
  51. ophyd_async/fastcs/panda/_utils.py +3 -2
  52. ophyd_async/fastcs/panda/_writer.py +28 -13
  53. ophyd_async/plan_stubs/_fly.py +16 -16
  54. ophyd_async/plan_stubs/_nd_attributes.py +12 -6
  55. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
  56. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +24 -20
  57. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
  58. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
  59. ophyd_async/sim/demo/_sim_motor.py +2 -1
  60. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/METADATA +46 -45
  61. ophyd_async-0.6.0.dist-info/RECORD +96 -0
  62. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/WHEEL +1 -1
  63. ophyd_async-0.5.2.dist-info/RECORD +0 -95
  64. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/LICENSE +0 -0
  65. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/entry_points.txt +0 -0
  66. {ophyd_async-0.5.2.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)
@@ -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)
@@ -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,5 +1,4 @@
1
1
  import asyncio
2
- from typing import FrozenSet, Set
3
2
 
4
3
  from ophyd_async.core import (
5
4
  DEFAULT_TIMEOUT,
@@ -14,7 +13,7 @@ from ._core_io import ADBaseIO, DetectorState
14
13
 
15
14
  # Default set of states that we should consider "good" i.e. the acquisition
16
15
  # is complete and went well
17
- DEFAULT_GOOD_STATES: FrozenSet[DetectorState] = frozenset(
16
+ DEFAULT_GOOD_STATES: frozenset[DetectorState] = frozenset(
18
17
  [DetectorState.Idle, DetectorState.Aborted]
19
18
  )
20
19
 
@@ -66,7 +65,7 @@ async def set_exposure_time_and_acquire_period_if_supplied(
66
65
 
67
66
  async def start_acquiring_driver_and_ensure_status(
68
67
  driver: ADBaseIO,
69
- good_states: Set[DetectorState] = set(DEFAULT_GOOD_STATES),
68
+ good_states: frozenset[DetectorState] = frozenset(DEFAULT_GOOD_STATES),
70
69
  timeout: float = DEFAULT_TIMEOUT,
71
70
  ) -> AsyncStatus:
72
71
  """
@@ -1,9 +1,10 @@
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,
@@ -42,19 +43,22 @@ class ADHDFWriter(DetectorWriter):
42
43
  self._dataset_describer = dataset_describer
43
44
 
44
45
  self._plugins = plugins
45
- self._capture_status: Optional[AsyncStatus] = None
46
- self._datasets: List[HDFDataset] = []
47
- self._file: Optional[HDFFile] = None
46
+ self._capture_status: AsyncStatus | None = None
47
+ self._datasets: list[HDFDataset] = []
48
+ self._file: HDFFile | None = None
48
49
  self._multiplier = 1
49
50
 
50
- async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
51
+ async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
51
52
  self._file = None
52
- info = self._path_provider(device_name=self.hdf.name)
53
+ info = self._path_provider(device_name=self._name_provider())
53
54
 
54
55
  # Set the directory creation depth first, since dir creation callback happens
55
56
  # when directory path PV is processed.
56
57
  await self.hdf.create_directory.set(info.create_dir_depth)
57
58
 
59
+ # Make sure we are using chunk auto-sizing
60
+ await asyncio.gather(self.hdf.chunk_size_auto.set(True))
61
+
58
62
  await asyncio.gather(
59
63
  self.hdf.num_extra_dims.set(0),
60
64
  self.hdf.lazy_open.set(True),
@@ -83,6 +87,9 @@ class ADHDFWriter(DetectorWriter):
83
87
  self._multiplier = multiplier
84
88
  outer_shape = (multiplier,) if multiplier > 1 else ()
85
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()
92
+
86
93
  # Add the main data
87
94
  self._datasets = [
88
95
  HDFDataset(
@@ -91,6 +98,7 @@ class ADHDFWriter(DetectorWriter):
91
98
  shape=detector_shape,
92
99
  dtype_numpy=np_dtype,
93
100
  multiplier=multiplier,
101
+ chunk_shape=(frames_per_chunk, *detector_shape),
94
102
  )
95
103
  ]
96
104
  # And all the scalar datasets
@@ -117,6 +125,9 @@ class ADHDFWriter(DetectorWriter):
117
125
  (),
118
126
  np_datatype,
119
127
  multiplier,
128
+ # NDAttributes appear to always be configured with
129
+ # this chunk size
130
+ chunk_shape=(16384,),
120
131
  )
121
132
  )
122
133
 
@@ -125,7 +136,7 @@ class ADHDFWriter(DetectorWriter):
125
136
  source=self.hdf.full_file_name.source,
126
137
  shape=outer_shape + tuple(ds.shape),
127
138
  dtype="array" if ds.shape else "number",
128
- dtype_numpy=ds.dtype_numpy,
139
+ dtype_numpy=ds.dtype_numpy, # type: ignore
129
140
  external="STREAM:",
130
141
  )
131
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)
@@ -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)
@@ -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),
@@ -1,12 +1,12 @@
1
1
  import asyncio
2
- from typing import Optional, Set
3
2
 
4
3
  from ophyd_async.core import (
5
4
  DEFAULT_TIMEOUT,
6
- AsyncStatus,
7
5
  DetectorControl,
8
6
  DetectorTrigger,
9
7
  )
8
+ from ophyd_async.core._detector import TriggerInfo
9
+ from ophyd_async.core._status import AsyncStatus
10
10
  from ophyd_async.epics import adcore
11
11
 
12
12
 
@@ -14,32 +14,37 @@ class SimController(DetectorControl):
14
14
  def __init__(
15
15
  self,
16
16
  driver: adcore.ADBaseIO,
17
- good_states: Set[adcore.DetectorState] = set(adcore.DEFAULT_GOOD_STATES),
17
+ good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
18
18
  ) -> None:
19
19
  self.driver = driver
20
20
  self.good_states = good_states
21
+ self.frame_timeout: float
22
+ self._arm_status: AsyncStatus | None = None
21
23
 
22
- def get_deadtime(self, exposure: float) -> float:
24
+ def get_deadtime(self, exposure: float | None) -> float:
23
25
  return 0.002
24
26
 
25
- async def arm(
26
- self,
27
- num: int,
28
- trigger: DetectorTrigger = DetectorTrigger.internal,
29
- exposure: Optional[float] = None,
30
- ) -> AsyncStatus:
27
+ async def prepare(self, trigger_info: TriggerInfo):
31
28
  assert (
32
- trigger == DetectorTrigger.internal
29
+ trigger_info.trigger == DetectorTrigger.internal
33
30
  ), "fly scanning (i.e. external triggering) is not supported for this device"
34
- frame_timeout = DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
31
+ self.frame_timeout = (
32
+ DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
33
+ )
35
34
  await asyncio.gather(
36
- self.driver.num_images.set(num),
35
+ self.driver.num_images.set(trigger_info.number),
37
36
  self.driver.image_mode.set(adcore.ImageMode.multiple),
38
37
  )
39
- return await adcore.start_acquiring_driver_and_ensure_status(
40
- self.driver, good_states=self.good_states, timeout=frame_timeout
38
+
39
+ async def arm(self):
40
+ self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
41
+ self.driver, good_states=self.good_states, timeout=self.frame_timeout
41
42
  )
42
43
 
44
+ async def wait_for_idle(self):
45
+ if self._arm_status:
46
+ await self._arm_status
47
+
43
48
  async def disarm(self):
44
49
  # We can't use caput callback as we already used it in arm() and we can't have
45
50
  # 2 or they will deadlock
@@ -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 ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource
@@ -27,32 +28,36 @@ class VimbaController(DetectorControl):
27
28
  driver: VimbaDriverIO,
28
29
  ) -> None:
29
30
  self._drv = driver
31
+ self._arm_status: AsyncStatus | None = None
30
32
 
31
- def get_deadtime(self, exposure: float) -> float:
33
+ def get_deadtime(self, exposure: float | None) -> float:
32
34
  return 0.001
33
35
 
34
- async def arm(
35
- self,
36
- num: int,
37
- trigger: DetectorTrigger = DetectorTrigger.internal,
38
- exposure: Optional[float] = None,
39
- ) -> AsyncStatus:
36
+ async def prepare(self, trigger_info: TriggerInfo):
40
37
  await asyncio.gather(
41
- self._drv.trigger_mode.set(TRIGGER_MODE[trigger]),
42
- self._drv.exposure_mode.set(EXPOSE_OUT_MODE[trigger]),
43
- self._drv.num_images.set(num),
38
+ self._drv.trigger_mode.set(TRIGGER_MODE[trigger_info.trigger]),
39
+ self._drv.exposure_mode.set(EXPOSE_OUT_MODE[trigger_info.trigger]),
40
+ self._drv.num_images.set(trigger_info.number),
44
41
  self._drv.image_mode.set(adcore.ImageMode.multiple),
45
42
  )
46
- if exposure is not None and trigger not in [
43
+ if trigger_info.livetime is not None and trigger_info.trigger not in [
47
44
  DetectorTrigger.variable_gate,
48
45
  DetectorTrigger.constant_gate,
49
46
  ]:
50
- await self._drv.acquire_time.set(exposure)
51
- if trigger != DetectorTrigger.internal:
47
+ await self._drv.acquire_time.set(trigger_info.livetime)
48
+ if trigger_info.trigger != DetectorTrigger.internal:
52
49
  self._drv.trigger_source.set(VimbaTriggerSource.line1)
53
50
  else:
54
51
  self._drv.trigger_source.set(VimbaTriggerSource.freerun)
55
- return await adcore.start_acquiring_driver_and_ensure_status(self._drv)
52
+
53
+ async def arm(self):
54
+ self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
55
+ self._drv
56
+ )
57
+
58
+ async def wait_for_idle(self):
59
+ if self._arm_status:
60
+ await self._arm_status
56
61
 
57
62
  async def disarm(self):
58
63
  await adcore.stop_busy_record(self._drv.acquire, False, timeout=1)
@@ -4,10 +4,10 @@ import numpy as np
4
4
  from bluesky.protocols import Movable, Stoppable
5
5
 
6
6
  from ophyd_async.core import (
7
+ CALCULATE_TIMEOUT,
7
8
  DEFAULT_TIMEOUT,
8
9
  AsyncStatus,
9
10
  CalculatableTimeout,
10
- CalculateTimeout,
11
11
  ConfigSignal,
12
12
  Device,
13
13
  HintedSignal,
@@ -44,9 +44,8 @@ class Mover(StandardReadable, Movable, Stoppable):
44
44
  self.readback.set_name(name)
45
45
 
46
46
  @WatchableAsyncStatus.wrap
47
- async def set(
48
- self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
49
- ):
47
+ async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT):
48
+ new_position = value
50
49
  self._set_success = True
51
50
  old_position, units, precision, velocity = await asyncio.gather(
52
51
  self.setpoint.get_value(),
@@ -54,7 +53,7 @@ class Mover(StandardReadable, Movable, Stoppable):
54
53
  self.precision.get_value(),
55
54
  self.velocity.get_value(),
56
55
  )
57
- if timeout is CalculateTimeout:
56
+ if timeout == CALCULATE_TIMEOUT:
58
57
  assert velocity > 0, "Mover has zero velocity"
59
58
  timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT
60
59
  # Make an Event that will be set on completion, and a Status that will
@@ -17,4 +17,3 @@ record(calc, "$(P)Value") {
17
17
  field(EGU, "$(EGU=cts/s)")
18
18
  field(PREC, "$(PREC=3)")
19
19
  }
20
-