ophyd-async 0.12.3__py3-none-any.whl → 0.13.1__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 (39) hide show
  1. ophyd_async/_version.py +16 -3
  2. ophyd_async/core/__init__.py +11 -0
  3. ophyd_async/core/_detector.py +7 -10
  4. ophyd_async/core/_enums.py +21 -0
  5. ophyd_async/core/_signal.py +9 -9
  6. ophyd_async/core/_utils.py +5 -4
  7. ophyd_async/epics/adandor/_andor.py +1 -2
  8. ophyd_async/epics/adaravis/__init__.py +1 -2
  9. ophyd_async/epics/adaravis/_aravis_controller.py +4 -4
  10. ophyd_async/epics/adaravis/_aravis_io.py +2 -12
  11. ophyd_async/epics/adcore/__init__.py +4 -2
  12. ophyd_async/epics/adcore/_core_detector.py +1 -2
  13. ophyd_async/epics/adcore/_core_io.py +60 -8
  14. ophyd_async/epics/adcore/_core_logic.py +4 -4
  15. ophyd_async/epics/adcore/_core_writer.py +10 -7
  16. ophyd_async/epics/adcore/_hdf_writer.py +12 -7
  17. ophyd_async/epics/adcore/_utils.py +38 -0
  18. ophyd_async/epics/adkinetix/_kinetix_io.py +4 -4
  19. ophyd_async/epics/adpilatus/_pilatus.py +2 -6
  20. ophyd_async/epics/advimba/__init__.py +0 -2
  21. ophyd_async/epics/advimba/_vimba_controller.py +6 -9
  22. ophyd_async/epics/advimba/_vimba_io.py +3 -10
  23. ophyd_async/epics/core/_aioca.py +6 -2
  24. ophyd_async/epics/core/_p4p.py +2 -3
  25. ophyd_async/epics/core/_pvi_connector.py +1 -1
  26. ophyd_async/epics/pmac/__init__.py +6 -1
  27. ophyd_async/epics/pmac/_pmac_io.py +1 -0
  28. ophyd_async/epics/pmac/_utils.py +231 -0
  29. ophyd_async/epics/testing/_example_ioc.py +1 -2
  30. ophyd_async/plan_stubs/_nd_attributes.py +11 -37
  31. ophyd_async/plan_stubs/_settings.py +1 -1
  32. ophyd_async/tango/core/_tango_transport.py +2 -2
  33. ophyd_async/testing/_assert.py +6 -6
  34. ophyd_async/testing/_one_of_everything.py +1 -1
  35. {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/METADATA +5 -4
  36. {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/RECORD +39 -37
  37. {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/WHEEL +0 -0
  38. {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/licenses/LICENSE +0 -0
  39. {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py CHANGED
@@ -1,7 +1,14 @@
1
1
  # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
5
12
 
6
13
  TYPE_CHECKING = False
7
14
  if TYPE_CHECKING:
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
9
16
  from typing import Union
10
17
 
11
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
12
20
  else:
13
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
14
23
 
15
24
  version: str
16
25
  __version__: str
17
26
  __version_tuple__: VERSION_TUPLE
18
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
19
30
 
20
- __version__ = version = '0.12.3'
21
- __version_tuple__ = version_tuple = (0, 12, 3)
31
+ __version__ = version = '0.13.1'
32
+ __version_tuple__ = version_tuple = (0, 13, 1)
33
+
34
+ __commit_id__ = commit_id = None
@@ -16,6 +16,12 @@ from ._detector import (
16
16
  )
17
17
  from ._device import Device, DeviceConnector, DeviceVector, init_devices
18
18
  from ._device_filler import DeviceFiller
19
+ from ._enums import (
20
+ EnabledDisabled,
21
+ EnableDisable,
22
+ InOut,
23
+ OnOff,
24
+ )
19
25
  from ._flyer import FlyerController, FlyMotorInfo, StandardFlyer
20
26
  from ._hdf_dataset import HDFDatasetDescription, HDFDocumentComposer
21
27
  from ._log import config_ophyd_async_logging
@@ -209,4 +215,9 @@ __all__ = [
209
215
  # Back compat - delete before 1.0
210
216
  "ConfigSignal",
211
217
  "HintedSignal",
218
+ # Standard enums
219
+ "EnabledDisabled",
220
+ "EnableDisable",
221
+ "InOut",
222
+ "OnOff",
212
223
  ]
@@ -84,7 +84,7 @@ class TriggerInfo(ConfinedModel):
84
84
  A exposures_per_event > 1 can be useful to have exposures from a faster detector
85
85
  able to be zipped with a single exposure from a slower detector. E.g. if
86
86
  number_of_events=10 and exposures_per_event=5 then the detector will take
87
- 10 exposures, but publish 2 StreamDatum indices, and describe() will show a
87
+ 50 exposures, but publish 10 StreamDatum indices, and describe() will show a
88
88
  shape of (5, h, w) for each.
89
89
  Default is 1.
90
90
  """
@@ -104,7 +104,11 @@ class DetectorController(ABC):
104
104
 
105
105
  @abstractmethod
106
106
  def get_deadtime(self, exposure: float | None) -> float:
107
- """For a given exposure, how long should the time between exposures be."""
107
+ """Get state-independent deadtime.
108
+
109
+ For a given exposure, what is the safest minimum time between exposures that
110
+ can be determined without reading signals.
111
+ """
108
112
 
109
113
  @abstractmethod
110
114
  async def prepare(self, trigger_info: TriggerInfo) -> None:
@@ -313,14 +317,7 @@ class StandardDetector(
313
317
  if value.trigger != DetectorTrigger.INTERNAL and not value.deadtime:
314
318
  msg = "Deadtime must be supplied when in externally triggered mode"
315
319
  raise ValueError(msg)
316
- required_deadtime = self._controller.get_deadtime(value.livetime)
317
- if value.deadtime and required_deadtime > value.deadtime:
318
- msg = (
319
- f"Detector {self._controller} needs at least {required_deadtime}s "
320
- f"deadtime, but trigger logic provides only {value.deadtime}s"
321
- )
322
- raise ValueError(msg)
323
- elif not value.deadtime:
320
+ if not value.deadtime:
324
321
  value.deadtime = self._controller.get_deadtime(value.livetime)
325
322
  self._trigger_info = value
326
323
  self._number_of_events_iter = iter(
@@ -0,0 +1,21 @@
1
+ from ._utils import StrictEnum
2
+
3
+
4
+ class OnOff(StrictEnum):
5
+ ON = "On"
6
+ OFF = "Off"
7
+
8
+
9
+ class EnableDisable(StrictEnum):
10
+ ENABLE = "Enable"
11
+ DISABLE = "Disable"
12
+
13
+
14
+ class EnabledDisabled(StrictEnum):
15
+ ENABLED = "Enabled"
16
+ DISABLED = "Disabled"
17
+
18
+
19
+ class InOut(StrictEnum):
20
+ IN = "In"
21
+ OUT = "Out"
@@ -39,8 +39,8 @@ from ._utils import (
39
39
  async def _wait_for(coro: Awaitable[T], timeout: float | None, source: str) -> T:
40
40
  try:
41
41
  return await asyncio.wait_for(coro, timeout)
42
- except asyncio.TimeoutError as e:
43
- raise asyncio.TimeoutError(source) from e
42
+ except TimeoutError as e:
43
+ raise TimeoutError(source) from e
44
44
 
45
45
 
46
46
  def _add_timeout(func):
@@ -492,7 +492,7 @@ async def observe_signals_value(
492
492
  last_item = ()
493
493
  while True:
494
494
  if overall_deadline and time.monotonic() >= overall_deadline:
495
- raise asyncio.TimeoutError(
495
+ raise TimeoutError(
496
496
  f"observe_value was still observing signals "
497
497
  f"{[signal.source for signal in signals]} after "
498
498
  f"timeout {done_timeout}s"
@@ -500,8 +500,8 @@ async def observe_signals_value(
500
500
  iteration_timeout = _get_iteration_timeout(timeout, overall_deadline)
501
501
  try:
502
502
  item = await asyncio.wait_for(q.get(), iteration_timeout)
503
- except asyncio.TimeoutError as exc:
504
- raise asyncio.TimeoutError(
503
+ except TimeoutError as exc:
504
+ raise TimeoutError(
505
505
  f"Timeout Error while waiting {iteration_timeout}s to update "
506
506
  f"{[signal.source for signal in signals]}. "
507
507
  f"Last observed signal and value were {last_item}"
@@ -536,8 +536,8 @@ class _ValueChecker(Generic[SignalDatatypeT]):
536
536
  ):
537
537
  try:
538
538
  await asyncio.wait_for(self._wait_for_value(signal), timeout)
539
- except asyncio.TimeoutError as e:
540
- raise asyncio.TimeoutError(
539
+ except TimeoutError as e:
540
+ raise TimeoutError(
541
541
  f"{signal.name} didn't match {self._matcher_name} in {timeout}s, "
542
542
  f"last value {self._last_value!r}"
543
543
  ) from e
@@ -635,8 +635,8 @@ async def set_and_wait_for_other_value(
635
635
  await asyncio.wait_for(_wait_for_value(), timeout)
636
636
  if wait_for_set_completion:
637
637
  await status
638
- except asyncio.TimeoutError as e:
639
- raise asyncio.TimeoutError(
638
+ except TimeoutError as e:
639
+ raise TimeoutError(
640
640
  f"{match_signal.name} value didn't match value from"
641
641
  f" {matcher.__name__}() in {timeout}s"
642
642
  ) from e
@@ -4,7 +4,7 @@ import asyncio
4
4
  import logging
5
5
  from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
6
6
  from dataclasses import dataclass
7
- from enum import Enum, EnumMeta
7
+ from enum import Enum, EnumMeta, StrEnum
8
8
  from typing import (
9
9
  Any,
10
10
  Generic,
@@ -59,15 +59,15 @@ class AnyStringUppercaseNameEnumMeta(UppercaseNameEnumMeta):
59
59
  return super().__call__(value, *args, **kwargs)
60
60
 
61
61
 
62
- class StrictEnum(str, Enum, metaclass=UppercaseNameEnumMeta):
62
+ class StrictEnum(StrEnum, metaclass=UppercaseNameEnumMeta):
63
63
  """All members should exist in the Backend, and there will be no extras."""
64
64
 
65
65
 
66
- class SubsetEnum(str, Enum, metaclass=AnyStringUppercaseNameEnumMeta):
66
+ class SubsetEnum(StrEnum, metaclass=AnyStringUppercaseNameEnumMeta):
67
67
  """All members should exist in the Backend, but there may be extras."""
68
68
 
69
69
 
70
- class SupersetEnum(str, Enum, metaclass=UppercaseNameEnumMeta):
70
+ class SupersetEnum(StrEnum, metaclass=UppercaseNameEnumMeta):
71
71
  """Some members should exist in the Backend, and there should be no extras."""
72
72
 
73
73
 
@@ -243,6 +243,7 @@ def get_enum_cls(datatype: type | None) -> type[EnumTypes] | None:
243
243
  """
244
244
  if get_origin(datatype) is Sequence:
245
245
  datatype = get_args(datatype)[0]
246
+ datatype = get_origin_class(datatype)
246
247
  if datatype and issubclass(datatype, Enum):
247
248
  if not issubclass(datatype, EnumTypes):
248
249
  raise TypeError(
@@ -1,7 +1,6 @@
1
1
  from collections.abc import Sequence
2
2
 
3
- from ophyd_async.core import PathProvider
4
- from ophyd_async.core._signal import SignalR
3
+ from ophyd_async.core import PathProvider, SignalR
5
4
  from ophyd_async.epics import adcore
6
5
 
7
6
  from ._andor_controller import Andor2Controller
@@ -5,12 +5,11 @@ https://github.com/areaDetector/ADAravis
5
5
 
6
6
  from ._aravis import AravisDetector
7
7
  from ._aravis_controller import AravisController
8
- from ._aravis_io import AravisDriverIO, AravisTriggerMode, AravisTriggerSource
8
+ from ._aravis_io import AravisDriverIO, AravisTriggerSource
9
9
 
10
10
  __all__ = [
11
11
  "AravisDetector",
12
12
  "AravisController",
13
13
  "AravisDriverIO",
14
- "AravisTriggerMode",
15
14
  "AravisTriggerSource",
16
15
  ]
@@ -1,9 +1,9 @@
1
1
  import asyncio
2
2
 
3
- from ophyd_async.core import DetectorTrigger, TriggerInfo
3
+ from ophyd_async.core import DetectorTrigger, OnOff, TriggerInfo
4
4
  from ophyd_async.epics import adcore
5
5
 
6
- from ._aravis_io import AravisDriverIO, AravisTriggerMode, AravisTriggerSource
6
+ from ._aravis_io import AravisDriverIO, AravisTriggerSource
7
7
 
8
8
  # The deadtime of an ADaravis controller varies depending on the exact model of camera.
9
9
  # Ideally we would maximize performance by dynamically retrieving the deadtime at
@@ -23,14 +23,14 @@ class AravisController(adcore.ADBaseController[AravisDriverIO]):
23
23
 
24
24
  if trigger_info.trigger is DetectorTrigger.INTERNAL:
25
25
  # Set trigger mode off to ignore the trigger source
26
- await self.driver.trigger_mode.set(AravisTriggerMode.OFF)
26
+ await self.driver.trigger_mode.set(OnOff.OFF)
27
27
  elif trigger_info.trigger in {
28
28
  DetectorTrigger.CONSTANT_GATE,
29
29
  DetectorTrigger.EDGE_TRIGGER,
30
30
  }:
31
31
  # Trigger on the rising edge of Line1
32
32
  # trigger mode must be set first and on it's own!
33
- await self.driver.trigger_mode.set(AravisTriggerMode.ON)
33
+ await self.driver.trigger_mode.set(OnOff.ON)
34
34
  await self.driver.trigger_source.set(AravisTriggerSource.LINE1)
35
35
  else:
36
36
  raise ValueError(f"ADAravis does not support {trigger_info.trigger}")
@@ -1,20 +1,10 @@
1
1
  from typing import Annotated as A
2
2
 
3
- from ophyd_async.core import SignalRW, StrictEnum, SubsetEnum
3
+ from ophyd_async.core import OnOff, SignalRW, SubsetEnum
4
4
  from ophyd_async.epics import adcore
5
5
  from ophyd_async.epics.core import PvSuffix
6
6
 
7
7
 
8
- class AravisTriggerMode(StrictEnum):
9
- """GigEVision GenICAM standard TriggerMode."""
10
-
11
- ON = "On"
12
- """Use TriggerSource to trigger each frame"""
13
-
14
- OFF = "Off"
15
- """Just trigger as fast as you can"""
16
-
17
-
18
8
  class AravisTriggerSource(SubsetEnum):
19
9
  """Which trigger source to use when TriggerMode=On."""
20
10
 
@@ -27,5 +17,5 @@ class AravisDriverIO(adcore.ADBaseIO):
27
17
  This mirrors the interface provided by ADAravis/db/aravisCamera.template.
28
18
  """
29
19
 
30
- trigger_mode: A[SignalRW[AravisTriggerMode], PvSuffix.rbv("TriggerMode")]
20
+ trigger_mode: A[SignalRW[OnOff], PvSuffix.rbv("TriggerMode")]
31
21
  trigger_source: A[SignalRW[AravisTriggerSource], PvSuffix.rbv("TriggerSource")]
@@ -7,7 +7,6 @@ from ._core_detector import AreaDetector, ContAcqAreaDetector
7
7
  from ._core_io import (
8
8
  ADBaseDatasetDescriber,
9
9
  ADBaseIO,
10
- ADCallbacks,
11
10
  ADCompression,
12
11
  ADState,
13
12
  NDArrayBaseIO,
@@ -18,6 +17,7 @@ from ._core_io import (
18
17
  NDPluginBaseIO,
19
18
  NDPluginCBIO,
20
19
  NDPluginStatsIO,
20
+ NDROIStatIO,
21
21
  )
22
22
  from ._core_logic import DEFAULT_GOOD_STATES, ADBaseContAcqController, ADBaseController
23
23
  from ._core_writer import ADWriter
@@ -33,11 +33,11 @@ from ._utils import (
33
33
  NDAttributeParam,
34
34
  NDAttributePv,
35
35
  NDAttributePvDbrType,
36
+ ndattributes_to_xml,
36
37
  )
37
38
 
38
39
  __all__ = [
39
40
  "ADBaseIO",
40
- "ADCallbacks",
41
41
  "ADCompression",
42
42
  "ADBaseContAcqController",
43
43
  "AreaDetector",
@@ -49,6 +49,7 @@ __all__ = [
49
49
  "NDFileHDFIO",
50
50
  "NDPluginBaseIO",
51
51
  "NDPluginStatsIO",
52
+ "NDROIStatIO",
52
53
  "DEFAULT_GOOD_STATES",
53
54
  "ADBaseDatasetDescriber",
54
55
  "ADBaseController",
@@ -66,4 +67,5 @@ __all__ = [
66
67
  "NDAttributePvDbrType",
67
68
  "NDCBFlushOnSoftTrgMode",
68
69
  "NDPluginCBIO",
70
+ "ndattributes_to_xml",
69
71
  ]
@@ -1,7 +1,6 @@
1
1
  from collections.abc import Sequence
2
2
 
3
- from ophyd_async.core import SignalR, StandardDetector
4
- from ophyd_async.core._providers import PathProvider
3
+ from ophyd_async.core import PathProvider, SignalR, StandardDetector
5
4
 
6
5
  from ._core_io import ADBaseIO, NDPluginBaseIO, NDPluginCBIO
7
6
  from ._core_logic import ADBaseContAcqController, ADBaseControllerT
@@ -1,17 +1,19 @@
1
1
  import asyncio
2
2
  from typing import Annotated as A
3
3
 
4
- from ophyd_async.core import DatasetDescriber, SignalR, SignalRW, StrictEnum
4
+ from ophyd_async.core import (
5
+ DatasetDescriber,
6
+ DeviceVector,
7
+ EnableDisable,
8
+ SignalR,
9
+ SignalRW,
10
+ StrictEnum,
11
+ )
5
12
  from ophyd_async.epics.core import EpicsDevice, PvSuffix
6
13
 
7
14
  from ._utils import ADBaseDataType, ADFileWriteMode, ADImageMode, convert_ad_dtype_to_np
8
15
 
9
16
 
10
- class ADCallbacks(StrictEnum):
11
- ENABLE = "Enable"
12
- DISABLE = "Disable"
13
-
14
-
15
17
  class NDArrayBaseIO(EpicsDevice):
16
18
  """Class responsible for passing detector data from drivers to pluglins.
17
19
 
@@ -53,7 +55,7 @@ class NDPluginBaseIO(NDArrayBaseIO):
53
55
  """
54
56
 
55
57
  nd_array_port: A[SignalRW[str], PvSuffix.rbv("NDArrayPort")]
56
- enable_callbacks: A[SignalRW[ADCallbacks], PvSuffix.rbv("EnableCallbacks")]
58
+ enable_callbacks: A[SignalRW[EnableDisable], PvSuffix.rbv("EnableCallbacks")]
57
59
  nd_array_address: A[SignalRW[int], PvSuffix.rbv("NDArrayAddress")]
58
60
  array_size0: A[SignalR[int], PvSuffix("ArraySize0_RBV")]
59
61
  array_size1: A[SignalR[int], PvSuffix("ArraySize1_RBV")]
@@ -87,6 +89,56 @@ class NDPluginStatsIO(NDPluginBaseIO):
87
89
  hist_max: A[SignalRW[float], PvSuffix.rbv("HistMax")]
88
90
 
89
91
 
92
+ class NDROIStatIO(NDPluginBaseIO):
93
+ """Plugin for calculating basic statistics for multiple ROIs.
94
+
95
+ Each ROI is implemented as an instance of NDROIStatNIO,
96
+ and the collection of ROIs is held as a DeviceVector.
97
+
98
+ See HTML docs at https://areadetector.github.io/areaDetector/ADCore/NDPluginROIStat.html
99
+ """
100
+
101
+ def __init__(self, prefix, num_channels=8, with_pvi=False, name=""):
102
+ self.channels = DeviceVector(
103
+ {i: NDROIStatNIO(f"{prefix}{i}:") for i in range(1, num_channels + 1)}
104
+ )
105
+ super().__init__(prefix, with_pvi, name)
106
+
107
+
108
+ class NDROIStatNIO(EpicsDevice):
109
+ """Defines the parameters for a single ROI used for statistics calculation.
110
+
111
+ Each instance represents a single ROI, with attributes for its position
112
+ (min_x, min_y) and size (size_x, size_y), as well as a name and use status.
113
+
114
+ See definition in ADApp/pluginSrc/NDPluginROIStat.h in https://github.com/areaDetector/ADCore.
115
+
116
+ Attributes:
117
+ name: The name of the ROI.
118
+ use: Flag indicating whether the ROI is used.
119
+ min_x: The start X-coordinate of the ROI.
120
+ min_y: The start Y-coordinate of the ROI.
121
+ size_x: The width of the ROI.
122
+ size_y: The height of the ROI.
123
+ min_value: Minimum count value in the ROI.
124
+ max_value: Maximum count value in the ROI.
125
+ mean_value: Mean counts value in the ROI.
126
+ total: Total counts in the ROI.
127
+ """
128
+
129
+ name_: A[SignalRW[str], PvSuffix("Name")]
130
+ use: A[SignalRW[bool], PvSuffix.rbv("Use")]
131
+ min_x: A[SignalRW[int], PvSuffix.rbv("MinX")]
132
+ min_y: A[SignalRW[int], PvSuffix.rbv("MinY")]
133
+ size_x: A[SignalRW[int], PvSuffix.rbv("SizeX")]
134
+ size_y: A[SignalRW[int], PvSuffix.rbv("SizeY")]
135
+ # stats
136
+ min_value: A[SignalR[float], PvSuffix("MinValue_RBV")]
137
+ max_value: A[SignalR[float], PvSuffix("MaxValue_RBV")]
138
+ mean_value: A[SignalR[float], PvSuffix("MeanValue_RBV")]
139
+ total: A[SignalR[float], PvSuffix("Total_RBV")]
140
+
141
+
90
142
  class ADState(StrictEnum):
91
143
  """Default set of states of an AreaDetector driver.
92
144
 
@@ -177,7 +229,7 @@ class NDFileHDFIO(NDFilePluginIO):
177
229
  swmr_mode: A[SignalRW[bool], PvSuffix.rbv("SWMRMode")]
178
230
  flush_now: A[SignalRW[bool], PvSuffix("FlushNow")]
179
231
  xml_file_name: A[SignalRW[str], PvSuffix.rbv("XMLFileName")]
180
- num_frames_chunks: A[SignalR[int], PvSuffix("NumFramesChunks_RBV")]
232
+ num_frames_chunks: A[SignalRW[int], PvSuffix.rbv("NumFramesChunks")]
181
233
  chunk_size_auto: A[SignalRW[bool], PvSuffix.rbv("ChunkSizeAuto")]
182
234
  lazy_open: A[SignalRW[bool], PvSuffix.rbv("LazyOpen")]
183
235
 
@@ -6,6 +6,7 @@ from ophyd_async.core import (
6
6
  AsyncStatus,
7
7
  DetectorController,
8
8
  DetectorTrigger,
9
+ EnableDisable,
9
10
  TriggerInfo,
10
11
  observe_value,
11
12
  set_and_wait_for_value,
@@ -14,7 +15,6 @@ from ophyd_async.epics.core import stop_busy_record
14
15
 
15
16
  from ._core_io import (
16
17
  ADBaseIO,
17
- ADCallbacks,
18
18
  ADState,
19
19
  NDCBFlushOnSoftTrgMode,
20
20
  NDPluginCBIO,
@@ -130,7 +130,7 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
130
130
  ):
131
131
  if state in self.good_states:
132
132
  return
133
- except asyncio.TimeoutError as exc:
133
+ except TimeoutError as exc:
134
134
  if state is not None:
135
135
  raise ValueError(
136
136
  f"Final detector state {state.value} not in valid end "
@@ -138,7 +138,7 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
138
138
  ) from exc
139
139
  else:
140
140
  # No updates from the detector, something else is wrong
141
- raise asyncio.TimeoutError(
141
+ raise TimeoutError(
142
142
  "Could not monitor detector state: "
143
143
  + self.driver.detector_state.source
144
144
  ) from exc
@@ -199,7 +199,7 @@ class ADBaseContAcqController(ADBaseController[ADBaseIO]):
199
199
 
200
200
  # Configure the CB plugin to collect the correct number of triggers
201
201
  await asyncio.gather(
202
- self.cb_plugin.enable_callbacks.set(ADCallbacks.ENABLE),
202
+ self.cb_plugin.enable_callbacks.set(EnableDisable.ENABLE),
203
203
  self.cb_plugin.pre_count.set(0),
204
204
  self.cb_plugin.post_count.set(trigger_info.total_number_of_exposures),
205
205
  self.cb_plugin.preset_trigger_count.set(1),
@@ -11,20 +11,23 @@ from event_model import ( # type: ignore
11
11
  )
12
12
  from pydantic import PositiveInt
13
13
 
14
- from ophyd_async.core._detector import DetectorWriter
15
- from ophyd_async.core._providers import DatasetDescriber, PathInfo, PathProvider
16
- from ophyd_async.core._signal import (
14
+ from ophyd_async.core import (
15
+ DEFAULT_TIMEOUT,
16
+ AsyncStatus,
17
+ DatasetDescriber,
18
+ DetectorWriter,
19
+ EnableDisable,
20
+ PathInfo,
21
+ PathProvider,
22
+ error_if_none,
17
23
  observe_value,
18
24
  set_and_wait_for_value,
19
25
  )
20
- from ophyd_async.core._status import AsyncStatus
21
- from ophyd_async.core._utils import DEFAULT_TIMEOUT, error_if_none
22
26
  from ophyd_async.epics.core import stop_busy_record
23
27
 
24
28
  # from ophyd_async.epics.adcore._core_logic import ADBaseDatasetDescriber
25
29
  from ._core_io import (
26
30
  ADBaseDatasetDescriber,
27
- ADCallbacks,
28
31
  NDArrayBaseIO,
29
32
  NDFileIO,
30
33
  NDFilePluginIO,
@@ -89,7 +92,7 @@ class ADWriter(DetectorWriter, Generic[NDFileIOT]):
89
92
  )
90
93
 
91
94
  if isinstance(self.fileio, NDFilePluginIO):
92
- await self.fileio.enable_callbacks.set(ADCallbacks.ENABLE)
95
+ await self.fileio.enable_callbacks.set(EnableDisable.ENABLE)
93
96
 
94
97
  # Set the directory creation depth first, since dir creation callback happens
95
98
  # when directory path PV is processed.
@@ -75,8 +75,13 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
75
75
  # Used by the base class
76
76
  self._exposures_per_event = exposures_per_event
77
77
 
78
- # Determine number of frames that will be saved per HDF chunk
78
+ # Determine number of frames that will be saved per HDF chunk.
79
+ # On a fresh IOC startup, this is set to zero until the first capture,
80
+ # so if it is zero, set it to 1.
79
81
  frames_per_chunk = await self.fileio.num_frames_chunks.get_value()
82
+ if frames_per_chunk == 0:
83
+ frames_per_chunk = 1
84
+ await self.fileio.num_frames_chunks.set(frames_per_chunk)
80
85
 
81
86
  if not _is_fully_described(detector_shape):
82
87
  # Questions:
@@ -99,12 +104,6 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
99
104
  )
100
105
  ]
101
106
 
102
- self._composer = HDFDocumentComposer(
103
- # See https://github.com/bluesky/ophyd-async/issues/122
104
- f"{self._path_info.directory_uri}{self._path_info.filename}{self._file_extension}",
105
- self._datasets,
106
- )
107
-
108
107
  # And all the scalar datasets
109
108
  for plugin in self._plugins.values():
110
109
  maybe_xml = await plugin.nd_attributes_file.get_value()
@@ -136,6 +135,12 @@ class ADHDFWriter(ADWriter[NDFileHDFIO]):
136
135
  )
137
136
  )
138
137
 
138
+ self._composer = HDFDocumentComposer(
139
+ # See https://github.com/bluesky/ophyd-async/issues/122
140
+ f"{self._path_info.directory_uri}{self._path_info.filename}{self._file_extension}",
141
+ self._datasets,
142
+ )
143
+
139
144
  describe = {
140
145
  ds.data_key: DataKey(
141
146
  source=self.fileio.full_file_name.source,
@@ -1,4 +1,6 @@
1
+ from collections.abc import Sequence
1
2
  from dataclasses import dataclass
3
+ from xml.etree import ElementTree as ET
2
4
 
3
5
  from ophyd_async.core import (
4
6
  SignalR,
@@ -130,3 +132,39 @@ class NDAttributeParam:
130
132
  datatype: NDAttributeDataType # The datatype of the parameter
131
133
  addr: int = 0 # The address as seen in the INP link of the record
132
134
  description: str = "" # A description that appears in the HDF file as an attribute
135
+
136
+
137
+ def ndattributes_to_xml(
138
+ ndattributes: Sequence[NDAttributeParam | NDAttributePv],
139
+ ) -> str:
140
+ """Convert a set of NDAttribute params to XML."""
141
+ root = ET.Element("Attributes")
142
+ for ndattribute in ndattributes:
143
+ if isinstance(ndattribute, NDAttributeParam):
144
+ ET.SubElement(
145
+ root,
146
+ "Attribute",
147
+ name=ndattribute.name,
148
+ type="PARAM",
149
+ source=ndattribute.param,
150
+ addr=str(ndattribute.addr),
151
+ datatype=ndattribute.datatype.value,
152
+ description=ndattribute.description,
153
+ )
154
+ elif isinstance(ndattribute, NDAttributePv):
155
+ ET.SubElement(
156
+ root,
157
+ "Attribute",
158
+ name=ndattribute.name,
159
+ type="EPICS_PV",
160
+ source=ndattribute.signal.source.split("ca://")[-1],
161
+ dbrtype=ndattribute.dbrtype.value,
162
+ description=ndattribute.description,
163
+ )
164
+ else:
165
+ raise ValueError(
166
+ f"Invalid type for ndattributes: {type(ndattribute)}. "
167
+ "Expected NDAttributePv or NDAttributeParam."
168
+ )
169
+ xml_text = ET.tostring(root, encoding="unicode")
170
+ return xml_text
@@ -16,10 +16,10 @@ class KinetixTriggerMode(StrictEnum):
16
16
  class KinetixReadoutMode(StrictEnum):
17
17
  """Readout mode for ADKinetix detector."""
18
18
 
19
- SENSITIVITY = 1
20
- SPEED = 2
21
- DYNAMIC_RANGE = 3
22
- SUB_ELECTRON = 4
19
+ SENSITIVITY = "1"
20
+ SPEED = "2"
21
+ DYNAMIC_RANGE = "3"
22
+ SUB_ELECTRON = "4"
23
23
 
24
24
 
25
25
  class KinetixDriverIO(adcore.ADBaseIO):
@@ -1,11 +1,7 @@
1
1
  from collections.abc import Sequence
2
2
 
3
- from ophyd_async.core import PathProvider
4
- from ophyd_async.core._signal import SignalR
5
- from ophyd_async.epics.adcore._core_detector import AreaDetector
6
- from ophyd_async.epics.adcore._core_io import NDPluginBaseIO
7
- from ophyd_async.epics.adcore._core_writer import ADWriter
8
- from ophyd_async.epics.adcore._hdf_writer import ADHDFWriter
3
+ from ophyd_async.core import PathProvider, SignalR
4
+ from ophyd_async.epics.adcore import ADHDFWriter, ADWriter, AreaDetector, NDPluginBaseIO
9
5
 
10
6
  from ._pilatus_controller import PilatusController, PilatusReadoutTime
11
7
  from ._pilatus_io import PilatusDriverIO
@@ -4,7 +4,6 @@ from ._vimba_io import (
4
4
  VimbaConvertFormat,
5
5
  VimbaDriverIO,
6
6
  VimbaExposeOutMode,
7
- VimbaOnOff,
8
7
  VimbaOverlap,
9
8
  VimbaTriggerSource,
10
9
  )
@@ -14,7 +13,6 @@ __all__ = [
14
13
  "VimbaController",
15
14
  "VimbaDriverIO",
16
15
  "VimbaExposeOutMode",
17
- "VimbaOnOff",
18
16
  "VimbaTriggerSource",
19
17
  "VimbaOverlap",
20
18
  "VimbaConvertFormat",