ophyd-async 0.8.0a5__py3-none-any.whl → 0.9.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 (116) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +17 -46
  3. ophyd_async/core/_detector.py +68 -44
  4. ophyd_async/core/_device.py +120 -79
  5. ophyd_async/core/_device_filler.py +17 -8
  6. ophyd_async/core/_flyer.py +2 -2
  7. ophyd_async/core/_protocol.py +0 -28
  8. ophyd_async/core/_readable.py +30 -23
  9. ophyd_async/core/_settings.py +104 -0
  10. ophyd_async/core/_signal.py +164 -151
  11. ophyd_async/core/_signal_backend.py +4 -1
  12. ophyd_async/core/_soft_signal_backend.py +2 -1
  13. ophyd_async/core/_table.py +27 -14
  14. ophyd_async/core/_utils.py +30 -5
  15. ophyd_async/core/_yaml_settings.py +64 -0
  16. ophyd_async/epics/adandor/__init__.py +9 -0
  17. ophyd_async/epics/adandor/_andor.py +45 -0
  18. ophyd_async/epics/adandor/_andor_controller.py +49 -0
  19. ophyd_async/epics/adandor/_andor_io.py +36 -0
  20. ophyd_async/epics/adaravis/__init__.py +3 -1
  21. ophyd_async/epics/adaravis/_aravis.py +23 -37
  22. ophyd_async/epics/adaravis/_aravis_controller.py +21 -30
  23. ophyd_async/epics/adaravis/_aravis_io.py +4 -4
  24. ophyd_async/epics/adcore/__init__.py +15 -8
  25. ophyd_async/epics/adcore/_core_detector.py +41 -0
  26. ophyd_async/epics/adcore/_core_io.py +56 -31
  27. ophyd_async/epics/adcore/_core_logic.py +99 -84
  28. ophyd_async/epics/adcore/_core_writer.py +219 -0
  29. ophyd_async/epics/adcore/_hdf_writer.py +33 -59
  30. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  31. ophyd_async/epics/adcore/_single_trigger.py +5 -4
  32. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  33. ophyd_async/epics/adcore/_utils.py +37 -36
  34. ophyd_async/epics/adkinetix/_kinetix.py +29 -24
  35. ophyd_async/epics/adkinetix/_kinetix_controller.py +15 -27
  36. ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
  37. ophyd_async/epics/adpilatus/__init__.py +2 -2
  38. ophyd_async/epics/adpilatus/_pilatus.py +28 -40
  39. ophyd_async/epics/adpilatus/_pilatus_controller.py +47 -25
  40. ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
  41. ophyd_async/epics/adsimdetector/__init__.py +3 -3
  42. ophyd_async/epics/adsimdetector/_sim.py +33 -17
  43. ophyd_async/epics/advimba/_vimba.py +23 -23
  44. ophyd_async/epics/advimba/_vimba_controller.py +21 -35
  45. ophyd_async/epics/advimba/_vimba_io.py +23 -23
  46. ophyd_async/epics/core/_aioca.py +52 -21
  47. ophyd_async/epics/core/_p4p.py +59 -16
  48. ophyd_async/epics/core/_pvi_connector.py +4 -2
  49. ophyd_async/epics/core/_signal.py +9 -2
  50. ophyd_async/epics/core/_util.py +10 -1
  51. ophyd_async/epics/eiger/_eiger_controller.py +10 -5
  52. ophyd_async/epics/eiger/_eiger_io.py +3 -3
  53. ophyd_async/epics/motor.py +26 -15
  54. ophyd_async/epics/sim/_ioc.py +29 -0
  55. ophyd_async/epics/{demo → sim}/_mover.py +12 -6
  56. ophyd_async/epics/{demo → sim}/_sensor.py +2 -2
  57. ophyd_async/epics/testing/__init__.py +24 -0
  58. ophyd_async/epics/testing/_example_ioc.py +91 -0
  59. ophyd_async/epics/testing/_utils.py +50 -0
  60. ophyd_async/epics/testing/test_records.db +174 -0
  61. ophyd_async/epics/testing/test_records_pva.db +177 -0
  62. ophyd_async/fastcs/core.py +2 -2
  63. ophyd_async/fastcs/panda/__init__.py +0 -2
  64. ophyd_async/fastcs/panda/_block.py +9 -9
  65. ophyd_async/fastcs/panda/_control.py +9 -4
  66. ophyd_async/fastcs/panda/_hdf_panda.py +7 -2
  67. ophyd_async/fastcs/panda/_table.py +4 -1
  68. ophyd_async/fastcs/panda/_trigger.py +7 -7
  69. ophyd_async/plan_stubs/__init__.py +14 -0
  70. ophyd_async/plan_stubs/_ensure_connected.py +11 -17
  71. ophyd_async/plan_stubs/_fly.py +2 -2
  72. ophyd_async/plan_stubs/_nd_attributes.py +7 -5
  73. ophyd_async/plan_stubs/_panda.py +13 -0
  74. ophyd_async/plan_stubs/_settings.py +125 -0
  75. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  76. ophyd_async/sim/__init__.py +19 -0
  77. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
  78. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
  79. ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +34 -32
  80. ophyd_async/tango/__init__.py +0 -43
  81. ophyd_async/tango/{signal → core}/__init__.py +7 -2
  82. ophyd_async/tango/{base_devices → core}/_base_device.py +38 -64
  83. ophyd_async/tango/{signal → core}/_signal.py +16 -4
  84. ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
  85. ophyd_async/tango/{signal → core}/_tango_transport.py +13 -15
  86. ophyd_async/tango/{demo → sim}/_counter.py +6 -7
  87. ophyd_async/tango/{demo → sim}/_mover.py +13 -9
  88. ophyd_async/testing/__init__.py +52 -0
  89. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  90. ophyd_async/testing/_assert.py +176 -0
  91. ophyd_async/{core → testing}/_mock_signal_utils.py +15 -11
  92. ophyd_async/testing/_one_of_everything.py +126 -0
  93. ophyd_async/testing/_wait_for_pending.py +22 -0
  94. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/METADATA +50 -48
  95. ophyd_async-0.9.0.dist-info/RECORD +129 -0
  96. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/WHEEL +1 -1
  97. ophyd_async/core/_device_save_loader.py +0 -274
  98. ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
  99. ophyd_async/fastcs/panda/_utils.py +0 -16
  100. ophyd_async/sim/demo/__init__.py +0 -19
  101. ophyd_async/sim/testing/__init__.py +0 -0
  102. ophyd_async/tango/base_devices/__init__.py +0 -4
  103. ophyd_async-0.8.0a5.dist-info/RECORD +0 -112
  104. ophyd_async-0.8.0a5.dist-info/entry_points.txt +0 -2
  105. /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
  106. /ophyd_async/epics/{demo → sim}/mover.db +0 -0
  107. /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
  108. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
  109. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
  110. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
  111. /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
  112. /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
  113. /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
  114. /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
  115. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/LICENSE +0 -0
  116. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/top_level.txt +0 -0
@@ -16,13 +16,38 @@ Callback = Callable[[T], None]
16
16
  DEFAULT_TIMEOUT = 10.0
17
17
  ErrorText = str | Mapping[str, Exception]
18
18
 
19
+ logger = logging.getLogger("ophyd_async")
19
20
 
20
- class StrictEnum(str, Enum):
21
+
22
+ class StrictEnumMeta(EnumMeta):
23
+ def __new__(metacls, *args, **kwargs):
24
+ ret = super().__new__(metacls, *args, **kwargs)
25
+ lowercase_names = [x.name for x in ret if not x.name.isupper()] # type: ignore
26
+ if lowercase_names:
27
+ raise TypeError(f"Names {lowercase_names} should be uppercase")
28
+ return ret
29
+
30
+
31
+ class StrictEnum(str, Enum, metaclass=StrictEnumMeta):
21
32
  """All members should exist in the Backend, and there will be no extras"""
22
33
 
23
34
 
24
- class SubsetEnumMeta(EnumMeta):
35
+ class SubsetEnumMeta(StrictEnumMeta):
25
36
  def __call__(self, value, *args, **kwargs): # type: ignore
37
+ """
38
+ Returns given value if it is a string and not a member of the enum.
39
+ If the value is not a string or is an enum member, default enum behavior
40
+ is applied. Type checking will complain if provided arbitrary string.
41
+
42
+ Returns:
43
+ Union[str, SubsetEnum]: If the value is a string and not a member of the
44
+ enum, the string is returned as is. Otherwise, the corresponding enum
45
+ member is returned.
46
+
47
+ Raises:
48
+ ValueError: If the value is not a string and cannot be converted to an enum
49
+ member.
50
+ """
26
51
  if isinstance(value, str) and not isinstance(value, self):
27
52
  return value
28
53
  return super().__call__(value, *args, **kwargs)
@@ -85,7 +110,7 @@ class NotConnected(Exception):
85
110
  def format_error_string(self, indent="") -> str:
86
111
  if not isinstance(self._errors, dict) and not isinstance(self._errors, str):
87
112
  raise RuntimeError(
88
- f"Unexpected type `{type(self._errors)}` " "expected `str` or `dict`"
113
+ f"Unexpected type `{type(self._errors)}` expected `str` or `dict`"
89
114
  )
90
115
 
91
116
  if isinstance(self._errors, str):
@@ -105,7 +130,7 @@ class NotConnected(Exception):
105
130
  ) -> NotConnected:
106
131
  for name, exception in exceptions.items():
107
132
  if not isinstance(exception, NotConnected):
108
- logging.exception(
133
+ logger.exception(
109
134
  f"device `{name}` raised unexpected exception "
110
135
  f"{type(exception).__name__}",
111
136
  exc_info=exception,
@@ -180,7 +205,7 @@ def get_enum_cls(datatype: type | None) -> type[StrictEnum] | None:
180
205
  if datatype and issubclass(datatype, Enum):
181
206
  if not issubclass(datatype, StrictEnum):
182
207
  raise TypeError(
183
- f"{datatype} should inherit from .SubsetEnum "
208
+ f"{datatype} should inherit from ophyd_async.core.SubsetEnum "
184
209
  "or ophyd_async.core.StrictEnum"
185
210
  )
186
211
  return datatype
@@ -0,0 +1,64 @@
1
+ import warnings
2
+ from enum import Enum
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import numpy as np
7
+ import numpy.typing as npt
8
+ import yaml
9
+ from pydantic import BaseModel
10
+
11
+ from ._settings import SettingsProvider
12
+
13
+
14
+ def ndarray_representer(dumper: yaml.Dumper, array: npt.NDArray[Any]) -> yaml.Node:
15
+ return dumper.represent_sequence(
16
+ "tag:yaml.org,2002:seq", array.tolist(), flow_style=True
17
+ )
18
+
19
+
20
+ def pydantic_model_abstraction_representer(
21
+ dumper: yaml.Dumper, model: BaseModel
22
+ ) -> yaml.Node:
23
+ return dumper.represent_data(model.model_dump(mode="python"))
24
+
25
+
26
+ def enum_representer(dumper: yaml.Dumper, enum: Enum) -> yaml.Node:
27
+ return dumper.represent_data(enum.value)
28
+
29
+
30
+ class YamlSettingsProvider(SettingsProvider):
31
+ def __init__(self, directory: Path | str):
32
+ self._directory = Path(directory)
33
+
34
+ def _file_path(self, name: str) -> Path:
35
+ return self._directory / (name + ".yaml")
36
+
37
+ async def store(self, name: str, data: dict[str, Any]):
38
+ yaml.add_representer(np.ndarray, ndarray_representer, Dumper=yaml.Dumper)
39
+ yaml.add_multi_representer(
40
+ BaseModel,
41
+ pydantic_model_abstraction_representer,
42
+ Dumper=yaml.Dumper,
43
+ )
44
+ yaml.add_multi_representer(Enum, enum_representer, Dumper=yaml.Dumper)
45
+ with open(self._file_path(name), "w") as file:
46
+ yaml.dump(data, file)
47
+
48
+ async def retrieve(self, name: str) -> dict[str, Any]:
49
+ with open(self._file_path(name)) as file:
50
+ data = yaml.full_load(file)
51
+ if isinstance(data, list):
52
+ warnings.warn(
53
+ DeprecationWarning(
54
+ "Found old save file. Re-save your yaml settings file "
55
+ f"{self._file_path(name)} using "
56
+ "ophyd_async.plan_stubs.store_settings"
57
+ ),
58
+ stacklevel=2,
59
+ )
60
+ merge = {}
61
+ for d in data:
62
+ merge.update(d)
63
+ return merge
64
+ return data
@@ -0,0 +1,9 @@
1
+ from ._andor import Andor2Detector
2
+ from ._andor_controller import Andor2Controller
3
+ from ._andor_io import Andor2DriverIO
4
+
5
+ __all__ = [
6
+ "Andor2Detector",
7
+ "Andor2Controller",
8
+ "Andor2DriverIO",
9
+ ]
@@ -0,0 +1,45 @@
1
+ from collections.abc import Sequence
2
+
3
+ from ophyd_async.core import PathProvider
4
+ from ophyd_async.core._signal import SignalR
5
+ from ophyd_async.epics import adcore
6
+
7
+ from ._andor_controller import Andor2Controller
8
+ from ._andor_io import Andor2DriverIO
9
+
10
+
11
+ class Andor2Detector(adcore.AreaDetector[Andor2Controller]):
12
+ """
13
+ Andor 2 area detector device (CCD detector 56fps with full chip readout).
14
+ Andor model:DU897_BV.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ prefix: str,
20
+ path_provider: PathProvider,
21
+ drv_suffix="cam1:",
22
+ writer_cls: type[adcore.ADWriter] = adcore.ADHDFWriter,
23
+ fileio_suffix: str | None = None,
24
+ name: str = "",
25
+ config_sigs: Sequence[SignalR] = (),
26
+ plugins: dict[str, adcore.NDPluginBaseIO] | None = None,
27
+ ):
28
+ driver = Andor2DriverIO(prefix + drv_suffix)
29
+ controller = Andor2Controller(driver)
30
+
31
+ writer = writer_cls.with_io(
32
+ prefix,
33
+ path_provider,
34
+ dataset_source=driver,
35
+ fileio_suffix=fileio_suffix,
36
+ plugins=plugins,
37
+ )
38
+
39
+ super().__init__(
40
+ controller=controller,
41
+ writer=writer,
42
+ plugins=plugins,
43
+ name=name,
44
+ config_sigs=config_sigs,
45
+ )
@@ -0,0 +1,49 @@
1
+ import asyncio
2
+
3
+ from ophyd_async.core import (
4
+ DetectorTrigger,
5
+ TriggerInfo,
6
+ )
7
+ from ophyd_async.epics import adcore
8
+
9
+ from ._andor_io import Andor2DriverIO, Andor2TriggerMode
10
+
11
+ _MIN_DEAD_TIME = 0.1
12
+ _MAX_NUM_IMAGE = 999_999
13
+
14
+
15
+ class Andor2Controller(adcore.ADBaseController[Andor2DriverIO]):
16
+ def __init__(
17
+ self,
18
+ driver: Andor2DriverIO,
19
+ good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
20
+ ) -> None:
21
+ super().__init__(driver, good_states=good_states)
22
+
23
+ def get_deadtime(self, exposure: float | None) -> float:
24
+ return _MIN_DEAD_TIME + (exposure or 0)
25
+
26
+ async def prepare(self, trigger_info: TriggerInfo):
27
+ await self.set_exposure_time_and_acquire_period_if_supplied(
28
+ trigger_info.livetime
29
+ )
30
+ await asyncio.gather(
31
+ self.driver.trigger_mode.set(self._get_trigger_mode(trigger_info.trigger)),
32
+ self.driver.num_images.set(
33
+ trigger_info.total_number_of_triggers or _MAX_NUM_IMAGE
34
+ ),
35
+ self.driver.image_mode.set(adcore.ImageMode.MULTIPLE),
36
+ )
37
+
38
+ def _get_trigger_mode(self, trigger: DetectorTrigger) -> Andor2TriggerMode:
39
+ supported_trigger_types = {
40
+ DetectorTrigger.INTERNAL: Andor2TriggerMode.INTERNAL,
41
+ DetectorTrigger.EDGE_TRIGGER: Andor2TriggerMode.EXT_TRIGGER,
42
+ }
43
+ if trigger not in supported_trigger_types:
44
+ raise ValueError(
45
+ f"{self.__class__.__name__} only supports the following trigger "
46
+ f"types: {supported_trigger_types} but was asked to "
47
+ f"use {trigger}"
48
+ )
49
+ return supported_trigger_types[trigger]
@@ -0,0 +1,36 @@
1
+ from ophyd_async.core import StrictEnum, SubsetEnum
2
+ from ophyd_async.epics.adcore import ADBaseIO
3
+ from ophyd_async.epics.core import (
4
+ epics_signal_r,
5
+ epics_signal_rw,
6
+ )
7
+
8
+
9
+ class Andor2TriggerMode(StrictEnum):
10
+ INTERNAL = "Internal"
11
+ EXT_TRIGGER = "External"
12
+ EXT_START = "External Start"
13
+ EXT_EXPOSURE = "External Exposure"
14
+ EXT_FVP = "External FVP"
15
+ SOFTWARE = "Software"
16
+
17
+
18
+ class Andor2DataType(SubsetEnum):
19
+ UINT16 = "UInt16"
20
+ UINT32 = "UInt32"
21
+ FLOAT32 = "Float32"
22
+ FLOAT64 = "Float64"
23
+
24
+
25
+ class Andor2DriverIO(ADBaseIO):
26
+ """
27
+ Epics pv for andor model:DU897_BV as deployed on p99
28
+ """
29
+
30
+ def __init__(self, prefix: str, name: str = "") -> None:
31
+ super().__init__(prefix, name=name)
32
+ self.trigger_mode = epics_signal_rw(Andor2TriggerMode, prefix + "TriggerMode")
33
+ self.data_type = epics_signal_r(Andor2DataType, prefix + "DataType_RBV")
34
+ self.andor_accumulate_period = epics_signal_r(
35
+ float, prefix + "AndorAccumulatePeriod_RBV"
36
+ )
@@ -1,9 +1,11 @@
1
1
  from ._aravis import AravisDetector
2
2
  from ._aravis_controller import AravisController
3
- from ._aravis_io import AravisDriverIO
3
+ from ._aravis_io import AravisDriverIO, AravisTriggerMode, AravisTriggerSource
4
4
 
5
5
  __all__ = [
6
6
  "AravisDetector",
7
7
  "AravisController",
8
8
  "AravisDriverIO",
9
+ "AravisTriggerMode",
10
+ "AravisTriggerSource",
9
11
  ]
@@ -1,61 +1,47 @@
1
- from typing import get_args
1
+ from collections.abc import Sequence
2
2
 
3
- from bluesky.protocols import HasHints, Hints
4
-
5
- from ophyd_async.core import PathProvider, StandardDetector
3
+ from ophyd_async.core import PathProvider
4
+ from ophyd_async.core._signal import SignalR
6
5
  from ophyd_async.epics import adcore
7
6
 
8
7
  from ._aravis_controller import AravisController
9
8
  from ._aravis_io import AravisDriverIO
10
9
 
11
10
 
12
- class AravisDetector(StandardDetector, HasHints):
11
+ class AravisDetector(adcore.AreaDetector[AravisController]):
13
12
  """
14
13
  Ophyd-async implementation of an ADAravis Detector.
15
14
  The detector may be configured for an external trigger on a GPIO port,
16
15
  which must be done prior to preparing the detector
17
16
  """
18
17
 
19
- _controller: AravisController
20
- _writer: adcore.ADHDFWriter
21
-
22
18
  def __init__(
23
19
  self,
24
20
  prefix: str,
25
21
  path_provider: PathProvider,
26
22
  drv_suffix="cam1:",
27
- hdf_suffix="HDF1:",
28
- name="",
23
+ writer_cls: type[adcore.ADWriter] = adcore.ADHDFWriter,
24
+ fileio_suffix: str | None = None,
25
+ name: str = "",
29
26
  gpio_number: AravisController.GPIO_NUMBER = 1,
27
+ config_sigs: Sequence[SignalR] = (),
28
+ plugins: dict[str, adcore.NDPluginBaseIO] | None = None,
30
29
  ):
31
- self.drv = AravisDriverIO(prefix + drv_suffix)
32
- self.hdf = adcore.NDFileHDFIO(prefix + hdf_suffix)
30
+ driver = AravisDriverIO(prefix + drv_suffix)
31
+ controller = AravisController(driver, gpio_number=gpio_number)
32
+
33
+ writer = writer_cls.with_io(
34
+ prefix,
35
+ path_provider,
36
+ dataset_source=driver,
37
+ fileio_suffix=fileio_suffix,
38
+ plugins=plugins,
39
+ )
33
40
 
34
41
  super().__init__(
35
- AravisController(self.drv, gpio_number=gpio_number),
36
- adcore.ADHDFWriter(
37
- self.hdf,
38
- path_provider,
39
- lambda: self.name,
40
- adcore.ADBaseDatasetDescriber(self.drv),
41
- ),
42
- config_sigs=(self.drv.acquire_time,),
42
+ controller=controller,
43
+ writer=writer,
44
+ plugins=plugins,
43
45
  name=name,
46
+ config_sigs=config_sigs,
44
47
  )
45
-
46
- def get_external_trigger_gpio(self):
47
- return self._controller.gpio_number
48
-
49
- def set_external_trigger_gpio(self, gpio_number: AravisController.GPIO_NUMBER):
50
- supported_gpio_numbers = get_args(AravisController.GPIO_NUMBER)
51
- if gpio_number not in supported_gpio_numbers:
52
- raise ValueError(
53
- f"{self.__class__.__name__} only supports the following GPIO "
54
- f"indices: {supported_gpio_numbers} but was asked to "
55
- f"use {gpio_number}"
56
- )
57
- self._controller.gpio_number = gpio_number
58
-
59
- @property
60
- def hints(self) -> Hints:
61
- return self._writer.hints
@@ -2,11 +2,8 @@ import asyncio
2
2
  from typing import Literal
3
3
 
4
4
  from ophyd_async.core import (
5
- AsyncStatus,
6
- DetectorController,
7
5
  DetectorTrigger,
8
6
  TriggerInfo,
9
- set_and_wait_for_value,
10
7
  )
11
8
  from ophyd_async.epics import adcore
12
9
 
@@ -18,49 +15,46 @@ from ._aravis_io import AravisDriverIO, AravisTriggerMode, AravisTriggerSource
18
15
  _HIGHEST_POSSIBLE_DEADTIME = 1961e-6
19
16
 
20
17
 
21
- class AravisController(DetectorController):
18
+ class AravisController(adcore.ADBaseController[AravisDriverIO]):
22
19
  GPIO_NUMBER = Literal[1, 2, 3, 4]
23
20
 
24
- def __init__(self, driver: AravisDriverIO, gpio_number: GPIO_NUMBER) -> None:
25
- self._drv = driver
21
+ def __init__(
22
+ self,
23
+ driver: AravisDriverIO,
24
+ good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
25
+ gpio_number: GPIO_NUMBER = 1,
26
+ ) -> None:
27
+ super().__init__(driver, good_states=good_states)
26
28
  self.gpio_number = gpio_number
27
- self._arm_status: AsyncStatus | None = None
28
29
 
29
30
  def get_deadtime(self, exposure: float | None) -> float:
30
31
  return _HIGHEST_POSSIBLE_DEADTIME
31
32
 
32
33
  async def prepare(self, trigger_info: TriggerInfo):
33
34
  if trigger_info.total_number_of_triggers == 0:
34
- image_mode = adcore.ImageMode.continuous
35
+ image_mode = adcore.ImageMode.CONTINUOUS
35
36
  else:
36
- image_mode = adcore.ImageMode.multiple
37
+ image_mode = adcore.ImageMode.MULTIPLE
37
38
  if (exposure := trigger_info.livetime) is not None:
38
- await self._drv.acquire_time.set(exposure)
39
+ await self.driver.acquire_time.set(exposure)
39
40
 
40
41
  trigger_mode, trigger_source = self._get_trigger_info(trigger_info.trigger)
41
42
  # trigger mode must be set first and on it's own!
42
- await self._drv.trigger_mode.set(trigger_mode)
43
+ await self.driver.trigger_mode.set(trigger_mode)
43
44
 
44
45
  await asyncio.gather(
45
- self._drv.trigger_source.set(trigger_source),
46
- self._drv.num_images.set(trigger_info.total_number_of_triggers),
47
- self._drv.image_mode.set(image_mode),
46
+ self.driver.trigger_source.set(trigger_source),
47
+ self.driver.num_images.set(trigger_info.total_number_of_triggers),
48
+ self.driver.image_mode.set(image_mode),
48
49
  )
49
50
 
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
56
-
57
51
  def _get_trigger_info(
58
52
  self, trigger: DetectorTrigger
59
53
  ) -> tuple[AravisTriggerMode, AravisTriggerSource]:
60
54
  supported_trigger_types = (
61
- DetectorTrigger.constant_gate,
62
- DetectorTrigger.edge_trigger,
63
- DetectorTrigger.internal,
55
+ DetectorTrigger.CONSTANT_GATE,
56
+ DetectorTrigger.EDGE_TRIGGER,
57
+ DetectorTrigger.INTERNAL,
64
58
  )
65
59
  if trigger not in supported_trigger_types:
66
60
  raise ValueError(
@@ -68,10 +62,7 @@ class AravisController(DetectorController):
68
62
  f"types: {supported_trigger_types} but was asked to "
69
63
  f"use {trigger}"
70
64
  )
71
- if trigger == DetectorTrigger.internal:
72
- return AravisTriggerMode.off, AravisTriggerSource.freerun
65
+ if trigger == DetectorTrigger.INTERNAL:
66
+ return AravisTriggerMode.OFF, AravisTriggerSource.FREERUN
73
67
  else:
74
- return (AravisTriggerMode.on, f"Line{self.gpio_number}") # type: ignore
75
-
76
- async def disarm(self):
77
- await adcore.stop_busy_record(self._drv.acquire, False, timeout=1)
68
+ return (AravisTriggerMode.ON, f"Line{self.gpio_number}") # type: ignore
@@ -6,8 +6,8 @@ from ophyd_async.epics.core import epics_signal_rw_rbv
6
6
  class AravisTriggerMode(StrictEnum):
7
7
  """GigEVision GenICAM standard: on=externally triggered"""
8
8
 
9
- on = "On"
10
- off = "Off"
9
+ ON = "On"
10
+ OFF = "Off"
11
11
 
12
12
 
13
13
  """A minimal set of TriggerSources that must be supported by the underlying record.
@@ -20,8 +20,8 @@ class AravisTriggerMode(StrictEnum):
20
20
 
21
21
 
22
22
  class AravisTriggerSource(SubsetEnum):
23
- freerun = "Freerun"
24
- line1 = "Line1"
23
+ FREERUN = "Freerun"
24
+ LINE1 = "Line1"
25
25
 
26
26
 
27
27
  class AravisDriverIO(adcore.ADBaseIO):
@@ -1,18 +1,20 @@
1
+ from ._core_detector import AreaDetector
1
2
  from ._core_io import (
3
+ ADBaseDatasetDescriber,
2
4
  ADBaseIO,
3
5
  DetectorState,
4
6
  NDArrayBaseIO,
5
7
  NDFileHDFIO,
8
+ NDFileIO,
9
+ NDPluginBaseIO,
6
10
  NDPluginStatsIO,
7
11
  )
8
- from ._core_logic import (
9
- DEFAULT_GOOD_STATES,
10
- ADBaseDatasetDescriber,
11
- set_exposure_time_and_acquire_period_if_supplied,
12
- start_acquiring_driver_and_ensure_status,
13
- )
12
+ from ._core_logic import DEFAULT_GOOD_STATES, ADBaseController
13
+ from ._core_writer import ADWriter
14
14
  from ._hdf_writer import ADHDFWriter
15
+ from ._jpeg_writer import ADJPEGWriter
15
16
  from ._single_trigger import SingleTriggerDetector
17
+ from ._tiff_writer import ADTIFFWriter
16
18
  from ._utils import (
17
19
  ADBaseDataType,
18
20
  FileWriteMode,
@@ -26,15 +28,20 @@ from ._utils import (
26
28
 
27
29
  __all__ = [
28
30
  "ADBaseIO",
31
+ "AreaDetector",
29
32
  "DetectorState",
30
33
  "NDArrayBaseIO",
34
+ "NDFileIO",
31
35
  "NDFileHDFIO",
36
+ "NDPluginBaseIO",
32
37
  "NDPluginStatsIO",
33
38
  "DEFAULT_GOOD_STATES",
34
39
  "ADBaseDatasetDescriber",
35
- "set_exposure_time_and_acquire_period_if_supplied",
36
- "start_acquiring_driver_and_ensure_status",
40
+ "ADBaseController",
41
+ "ADWriter",
37
42
  "ADHDFWriter",
43
+ "ADTIFFWriter",
44
+ "ADJPEGWriter",
38
45
  "SingleTriggerDetector",
39
46
  "ADBaseDataType",
40
47
  "FileWriteMode",
@@ -0,0 +1,41 @@
1
+ from collections.abc import Sequence
2
+
3
+ from ophyd_async.core import SignalR, StandardDetector
4
+
5
+ from ._core_io import NDPluginBaseIO
6
+ from ._core_logic import ADBaseControllerT
7
+ from ._core_writer import ADWriter
8
+
9
+
10
+ class AreaDetector(StandardDetector[ADBaseControllerT, ADWriter]):
11
+ def __init__(
12
+ self,
13
+ controller: ADBaseControllerT,
14
+ writer: ADWriter,
15
+ plugins: dict[str, NDPluginBaseIO] | None = None,
16
+ config_sigs: Sequence[SignalR] = (),
17
+ name: str = "",
18
+ ):
19
+ self.driver = controller.driver
20
+ self.fileio = writer.fileio
21
+
22
+ if plugins is not None:
23
+ for name, plugin in plugins.items():
24
+ setattr(self, name, plugin)
25
+
26
+ super().__init__(
27
+ controller,
28
+ writer,
29
+ (self.driver.acquire_period, self.driver.acquire_time, *config_sigs),
30
+ name=name,
31
+ )
32
+
33
+ def get_plugin(
34
+ self, name: str, plugin_type: type[NDPluginBaseIO] = NDPluginBaseIO
35
+ ) -> NDPluginBaseIO:
36
+ plugin = getattr(self, name, None)
37
+ if not isinstance(plugin, plugin_type):
38
+ raise TypeError(
39
+ f"Expected {self.name}.{name} to be a {plugin_type}, got {plugin}"
40
+ )
41
+ return plugin