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.
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +17 -46
- ophyd_async/core/_detector.py +68 -44
- ophyd_async/core/_device.py +120 -79
- ophyd_async/core/_device_filler.py +17 -8
- ophyd_async/core/_flyer.py +2 -2
- ophyd_async/core/_protocol.py +0 -28
- ophyd_async/core/_readable.py +30 -23
- ophyd_async/core/_settings.py +104 -0
- ophyd_async/core/_signal.py +164 -151
- ophyd_async/core/_signal_backend.py +4 -1
- ophyd_async/core/_soft_signal_backend.py +2 -1
- ophyd_async/core/_table.py +27 -14
- ophyd_async/core/_utils.py +30 -5
- ophyd_async/core/_yaml_settings.py +64 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +49 -0
- ophyd_async/epics/adandor/_andor_io.py +36 -0
- ophyd_async/epics/adaravis/__init__.py +3 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -37
- ophyd_async/epics/adaravis/_aravis_controller.py +21 -30
- ophyd_async/epics/adaravis/_aravis_io.py +4 -4
- ophyd_async/epics/adcore/__init__.py +15 -8
- ophyd_async/epics/adcore/_core_detector.py +41 -0
- ophyd_async/epics/adcore/_core_io.py +56 -31
- ophyd_async/epics/adcore/_core_logic.py +99 -84
- ophyd_async/epics/adcore/_core_writer.py +219 -0
- ophyd_async/epics/adcore/_hdf_writer.py +33 -59
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +5 -4
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +37 -36
- ophyd_async/epics/adkinetix/_kinetix.py +29 -24
- ophyd_async/epics/adkinetix/_kinetix_controller.py +15 -27
- ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
- ophyd_async/epics/adpilatus/__init__.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +47 -25
- ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
- ophyd_async/epics/adsimdetector/__init__.py +3 -3
- ophyd_async/epics/adsimdetector/_sim.py +33 -17
- ophyd_async/epics/advimba/_vimba.py +23 -23
- ophyd_async/epics/advimba/_vimba_controller.py +21 -35
- ophyd_async/epics/advimba/_vimba_io.py +23 -23
- ophyd_async/epics/core/_aioca.py +52 -21
- ophyd_async/epics/core/_p4p.py +59 -16
- ophyd_async/epics/core/_pvi_connector.py +4 -2
- ophyd_async/epics/core/_signal.py +9 -2
- ophyd_async/epics/core/_util.py +10 -1
- ophyd_async/epics/eiger/_eiger_controller.py +10 -5
- ophyd_async/epics/eiger/_eiger_io.py +3 -3
- ophyd_async/epics/motor.py +26 -15
- ophyd_async/epics/sim/_ioc.py +29 -0
- ophyd_async/epics/{demo → sim}/_mover.py +12 -6
- ophyd_async/epics/{demo → sim}/_sensor.py +2 -2
- ophyd_async/epics/testing/__init__.py +24 -0
- ophyd_async/epics/testing/_example_ioc.py +91 -0
- ophyd_async/epics/testing/_utils.py +50 -0
- ophyd_async/epics/testing/test_records.db +174 -0
- ophyd_async/epics/testing/test_records_pva.db +177 -0
- ophyd_async/fastcs/core.py +2 -2
- ophyd_async/fastcs/panda/__init__.py +0 -2
- ophyd_async/fastcs/panda/_block.py +9 -9
- ophyd_async/fastcs/panda/_control.py +9 -4
- ophyd_async/fastcs/panda/_hdf_panda.py +7 -2
- ophyd_async/fastcs/panda/_table.py +4 -1
- ophyd_async/fastcs/panda/_trigger.py +7 -7
- ophyd_async/plan_stubs/__init__.py +14 -0
- ophyd_async/plan_stubs/_ensure_connected.py +11 -17
- ophyd_async/plan_stubs/_fly.py +2 -2
- ophyd_async/plan_stubs/_nd_attributes.py +7 -5
- ophyd_async/plan_stubs/_panda.py +13 -0
- ophyd_async/plan_stubs/_settings.py +125 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +19 -0
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
- ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +34 -32
- ophyd_async/tango/__init__.py +0 -43
- ophyd_async/tango/{signal → core}/__init__.py +7 -2
- ophyd_async/tango/{base_devices → core}/_base_device.py +38 -64
- ophyd_async/tango/{signal → core}/_signal.py +16 -4
- ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
- ophyd_async/tango/{signal → core}/_tango_transport.py +13 -15
- ophyd_async/tango/{demo → sim}/_counter.py +6 -7
- ophyd_async/tango/{demo → sim}/_mover.py +13 -9
- ophyd_async/testing/__init__.py +52 -0
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +176 -0
- ophyd_async/{core → testing}/_mock_signal_utils.py +15 -11
- ophyd_async/testing/_one_of_everything.py +126 -0
- ophyd_async/testing/_wait_for_pending.py +22 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/METADATA +50 -48
- ophyd_async-0.9.0.dist-info/RECORD +129 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async/tango/base_devices/__init__.py +0 -4
- ophyd_async-0.8.0a5.dist-info/RECORD +0 -112
- ophyd_async-0.8.0a5.dist-info/entry_points.txt +0 -2
- /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
- /ophyd_async/epics/{demo → sim}/mover.db +0 -0
- /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
- /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/top_level.txt +0 -0
ophyd_async/core/_utils.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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)}`
|
|
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
|
-
|
|
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,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
|
|
1
|
+
from collections.abc import Sequence
|
|
2
2
|
|
|
3
|
-
from
|
|
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(
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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(
|
|
18
|
+
class AravisController(adcore.ADBaseController[AravisDriverIO]):
|
|
22
19
|
GPIO_NUMBER = Literal[1, 2, 3, 4]
|
|
23
20
|
|
|
24
|
-
def __init__(
|
|
25
|
-
self
|
|
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.
|
|
35
|
+
image_mode = adcore.ImageMode.CONTINUOUS
|
|
35
36
|
else:
|
|
36
|
-
image_mode = adcore.ImageMode.
|
|
37
|
+
image_mode = adcore.ImageMode.MULTIPLE
|
|
37
38
|
if (exposure := trigger_info.livetime) is not None:
|
|
38
|
-
await self.
|
|
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.
|
|
43
|
+
await self.driver.trigger_mode.set(trigger_mode)
|
|
43
44
|
|
|
44
45
|
await asyncio.gather(
|
|
45
|
-
self.
|
|
46
|
-
self.
|
|
47
|
-
self.
|
|
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.
|
|
62
|
-
DetectorTrigger.
|
|
63
|
-
DetectorTrigger.
|
|
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.
|
|
72
|
-
return AravisTriggerMode.
|
|
65
|
+
if trigger == DetectorTrigger.INTERNAL:
|
|
66
|
+
return AravisTriggerMode.OFF, AravisTriggerSource.FREERUN
|
|
73
67
|
else:
|
|
74
|
-
return (AravisTriggerMode.
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
36
|
-
"
|
|
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
|