ophyd-async 0.1.0__py3-none-any.whl → 0.3a1__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 +47 -12
- ophyd_async/core/_providers.py +66 -0
- ophyd_async/core/async_status.py +7 -5
- ophyd_async/core/detector.py +321 -0
- ophyd_async/core/device.py +184 -0
- ophyd_async/core/device_save_loader.py +286 -0
- ophyd_async/core/flyer.py +94 -0
- ophyd_async/core/{_device/_signal/signal.py → signal.py} +46 -18
- ophyd_async/core/{_device/_backend/signal_backend.py → signal_backend.py} +6 -2
- ophyd_async/core/{_device/_backend/sim_signal_backend.py → sim_signal_backend.py} +6 -2
- ophyd_async/core/{_device/standard_readable.py → standard_readable.py} +3 -3
- ophyd_async/core/utils.py +79 -29
- ophyd_async/epics/_backend/_aioca.py +38 -25
- ophyd_async/epics/_backend/_p4p.py +62 -27
- ophyd_async/epics/_backend/common.py +20 -0
- ophyd_async/epics/areadetector/__init__.py +10 -13
- ophyd_async/epics/areadetector/controllers/__init__.py +4 -0
- ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +52 -0
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +49 -0
- ophyd_async/epics/areadetector/drivers/__init__.py +15 -0
- ophyd_async/epics/areadetector/drivers/ad_base.py +111 -0
- ophyd_async/epics/areadetector/drivers/pilatus_driver.py +18 -0
- ophyd_async/epics/areadetector/single_trigger_det.py +4 -4
- ophyd_async/epics/areadetector/utils.py +91 -3
- ophyd_async/epics/areadetector/writers/__init__.py +5 -0
- ophyd_async/epics/areadetector/writers/_hdfdataset.py +10 -0
- ophyd_async/epics/areadetector/writers/_hdffile.py +54 -0
- ophyd_async/epics/areadetector/writers/hdf_writer.py +133 -0
- ophyd_async/epics/areadetector/{nd_file_hdf.py → writers/nd_file_hdf.py} +22 -5
- ophyd_async/epics/areadetector/writers/nd_plugin.py +30 -0
- ophyd_async/epics/demo/__init__.py +3 -2
- ophyd_async/epics/demo/demo_ad_sim_detector.py +35 -0
- ophyd_async/epics/motion/motor.py +2 -1
- ophyd_async/epics/pvi.py +70 -0
- ophyd_async/epics/signal/__init__.py +0 -2
- ophyd_async/epics/signal/signal.py +1 -1
- ophyd_async/panda/__init__.py +12 -8
- ophyd_async/panda/panda.py +43 -134
- ophyd_async/panda/panda_controller.py +41 -0
- ophyd_async/panda/table.py +158 -0
- ophyd_async/panda/utils.py +15 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/METADATA +49 -42
- ophyd_async-0.3a1.dist-info/RECORD +56 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device/__init__.py +0 -0
- ophyd_async/core/_device/_backend/__init__.py +0 -0
- ophyd_async/core/_device/_signal/__init__.py +0 -0
- ophyd_async/core/_device/device.py +0 -60
- ophyd_async/core/_device/device_collector.py +0 -121
- ophyd_async/core/_device/device_vector.py +0 -14
- ophyd_async/epics/areadetector/ad_driver.py +0 -18
- ophyd_async/epics/areadetector/directory_provider.py +0 -18
- ophyd_async/epics/areadetector/hdf_streamer_det.py +0 -167
- ophyd_async/epics/areadetector/nd_plugin.py +0 -13
- ophyd_async/epics/signal/pvi_get.py +0 -22
- ophyd_async-0.1.0.dist-info/RECORD +0 -45
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/LICENSE +0 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import FrozenSet, Sequence, Set
|
|
4
|
+
|
|
5
|
+
from ophyd_async.core import (
|
|
6
|
+
DEFAULT_TIMEOUT,
|
|
7
|
+
AsyncStatus,
|
|
8
|
+
ShapeProvider,
|
|
9
|
+
set_and_wait_for_value,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from ...signal.signal import epics_signal_rw
|
|
13
|
+
from ..utils import ImageMode, ad_r, ad_rw
|
|
14
|
+
from ..writers.nd_plugin import NDArrayBase
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DetectorState(str, Enum):
|
|
18
|
+
"""
|
|
19
|
+
Default set of states of an AreaDetector driver.
|
|
20
|
+
See definition in ADApp/ADSrc/ADDriver.h in https://github.com/areaDetector/ADCore
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
Idle = "Idle"
|
|
24
|
+
Acquire = "Acquire"
|
|
25
|
+
Readout = "Readout"
|
|
26
|
+
Correct = "Correct"
|
|
27
|
+
Saving = "Saving"
|
|
28
|
+
Aborting = "Aborting"
|
|
29
|
+
Error = "Error"
|
|
30
|
+
Waiting = "Waiting"
|
|
31
|
+
Initializing = "Initializing"
|
|
32
|
+
Disconnected = "Disconnected"
|
|
33
|
+
Aborted = "Aborted"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
#: Default set of states that we should consider "good" i.e. the acquisition
|
|
37
|
+
# is complete and went well
|
|
38
|
+
DEFAULT_GOOD_STATES: FrozenSet[DetectorState] = frozenset(
|
|
39
|
+
[DetectorState.Idle, DetectorState.Aborted]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ADBase(NDArrayBase):
|
|
44
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
45
|
+
# Define some signals
|
|
46
|
+
self.acquire = ad_rw(bool, prefix + "Acquire")
|
|
47
|
+
self.acquire_time = ad_rw(float, prefix + "AcquireTime")
|
|
48
|
+
self.num_images = ad_rw(int, prefix + "NumImages")
|
|
49
|
+
self.image_mode = ad_rw(ImageMode, prefix + "ImageMode")
|
|
50
|
+
self.array_counter = ad_rw(int, prefix + "ArrayCounter")
|
|
51
|
+
self.array_size_x = ad_r(int, prefix + "ArraySizeX")
|
|
52
|
+
self.array_size_y = ad_r(int, prefix + "ArraySizeY")
|
|
53
|
+
self.detector_state = ad_r(DetectorState, prefix + "DetectorState")
|
|
54
|
+
# There is no _RBV for this one
|
|
55
|
+
self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins")
|
|
56
|
+
super().__init__(prefix, name=name)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def start_acquiring_driver_and_ensure_status(
|
|
60
|
+
driver: ADBase,
|
|
61
|
+
good_states: Set[DetectorState] = set(DEFAULT_GOOD_STATES),
|
|
62
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
63
|
+
) -> AsyncStatus:
|
|
64
|
+
"""
|
|
65
|
+
Start acquiring driver, raising ValueError if the detector is in a bad state.
|
|
66
|
+
|
|
67
|
+
This sets driver.acquire to True, and waits for it to be True up to a timeout.
|
|
68
|
+
Then, it checks that the DetectorState PV is in DEFAULT_GOOD_STATES, and otherwise
|
|
69
|
+
raises a ValueError.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
driver:
|
|
74
|
+
The driver to start acquiring. Must subclass ADBase.
|
|
75
|
+
good_states:
|
|
76
|
+
set of states defined in DetectorState enum which are considered good states.
|
|
77
|
+
timeout:
|
|
78
|
+
How long to wait for driver.acquire to readback True (i.e. acquiring).
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
AsyncStatus:
|
|
83
|
+
An AsyncStatus that can be awaited to set driver.acquire to True and perform
|
|
84
|
+
subsequent raising (if applicable) due to detector state.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
status = await set_and_wait_for_value(driver.acquire, True, timeout=timeout)
|
|
88
|
+
|
|
89
|
+
async def complete_acquisition() -> None:
|
|
90
|
+
"""NOTE: possible race condition here between the callback from
|
|
91
|
+
set_and_wait_for_value and the detector state updating."""
|
|
92
|
+
await status
|
|
93
|
+
state = await driver.detector_state.get_value()
|
|
94
|
+
if state not in good_states:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Final detector state {state} not in valid end states: {good_states}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return AsyncStatus(complete_acquisition())
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ADBaseShapeProvider(ShapeProvider):
|
|
103
|
+
def __init__(self, driver: ADBase) -> None:
|
|
104
|
+
self._driver = driver
|
|
105
|
+
|
|
106
|
+
async def __call__(self) -> Sequence[int]:
|
|
107
|
+
shape = await asyncio.gather(
|
|
108
|
+
self._driver.array_size_y.get_value(),
|
|
109
|
+
self._driver.array_size_x.get_value(),
|
|
110
|
+
)
|
|
111
|
+
return shape
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from ..utils import ad_rw
|
|
4
|
+
from .ad_base import ADBase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TriggerMode(str, Enum):
|
|
8
|
+
internal = "Internal"
|
|
9
|
+
ext_enable = "Ext. Enable"
|
|
10
|
+
ext_trigger = "Ext. Trigger"
|
|
11
|
+
mult_trigger = "Mult. Trigger"
|
|
12
|
+
alignment = "Alignment"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PilatusDriver(ADBase):
|
|
16
|
+
def __init__(self, prefix: str) -> None:
|
|
17
|
+
self.trigger_mode = ad_rw(TriggerMode, prefix + "TriggerMode")
|
|
18
|
+
super().__init__(prefix)
|
|
@@ -5,18 +5,18 @@ from bluesky.protocols import Triggerable
|
|
|
5
5
|
|
|
6
6
|
from ophyd_async.core import AsyncStatus, SignalR, StandardReadable
|
|
7
7
|
|
|
8
|
-
from .
|
|
9
|
-
from .nd_plugin import NDPlugin
|
|
8
|
+
from .drivers.ad_base import ADBase
|
|
10
9
|
from .utils import ImageMode
|
|
10
|
+
from .writers.nd_plugin import NDPluginBase
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class SingleTriggerDet(StandardReadable, Triggerable):
|
|
14
14
|
def __init__(
|
|
15
15
|
self,
|
|
16
|
-
drv:
|
|
16
|
+
drv: ADBase,
|
|
17
17
|
read_uncached: Sequence[SignalR] = (),
|
|
18
18
|
name="",
|
|
19
|
-
**plugins:
|
|
19
|
+
**plugins: NDPluginBase,
|
|
20
20
|
) -> None:
|
|
21
21
|
self.drv = drv
|
|
22
22
|
self.__dict__.update(plugins)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from typing import Type
|
|
2
|
+
from typing import Optional, Type
|
|
3
|
+
from xml.etree import cElementTree as ET
|
|
3
4
|
|
|
4
|
-
from ophyd_async.core import SignalR, SignalRW, T
|
|
5
|
+
from ophyd_async.core import DEFAULT_TIMEOUT, SignalR, SignalRW, T, wait_for_value
|
|
5
6
|
|
|
6
7
|
from ..signal.signal import epics_signal_r, epics_signal_rw
|
|
7
8
|
|
|
@@ -20,7 +21,94 @@ class FileWriteMode(str, Enum):
|
|
|
20
21
|
stream = "Stream"
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
class ImageMode(Enum):
|
|
24
|
+
class ImageMode(str, Enum):
|
|
24
25
|
single = "Single"
|
|
25
26
|
multiple = "Multiple"
|
|
26
27
|
continuous = "Continuous"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NDAttributeDataType(str, Enum):
|
|
31
|
+
INT = "INT"
|
|
32
|
+
DOUBLE = "DOUBLE"
|
|
33
|
+
STRING = "STRING"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class NDAttributesXML:
|
|
37
|
+
"""Helper to make NDAttributesFile XML for areaDetector"""
|
|
38
|
+
|
|
39
|
+
_dbr_types = {
|
|
40
|
+
None: "DBR_NATIVE",
|
|
41
|
+
NDAttributeDataType.INT: "DBR_LONG",
|
|
42
|
+
NDAttributeDataType.DOUBLE: "DBR_DOUBLE",
|
|
43
|
+
NDAttributeDataType.STRING: "DBR_STRING",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def __init__(self):
|
|
47
|
+
self._root = ET.Element("Attributes")
|
|
48
|
+
|
|
49
|
+
def add_epics_pv(
|
|
50
|
+
self,
|
|
51
|
+
name: str,
|
|
52
|
+
pv: str,
|
|
53
|
+
datatype: Optional[NDAttributeDataType] = None,
|
|
54
|
+
description: str = "",
|
|
55
|
+
):
|
|
56
|
+
"""Add a PV to the attribute list
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
name: The attribute name
|
|
60
|
+
pv: The pv to get from
|
|
61
|
+
datatype: An override datatype, otherwise will use native EPICS type
|
|
62
|
+
description: A description that appears in the HDF file as an attribute
|
|
63
|
+
"""
|
|
64
|
+
ET.SubElement(
|
|
65
|
+
self._root,
|
|
66
|
+
"Attribute",
|
|
67
|
+
name=name,
|
|
68
|
+
type="EPICS_PV",
|
|
69
|
+
source=pv,
|
|
70
|
+
datatype=self._dbr_types[datatype],
|
|
71
|
+
description=description,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def add_param(
|
|
75
|
+
self,
|
|
76
|
+
name: str,
|
|
77
|
+
param: str,
|
|
78
|
+
datatype: NDAttributeDataType,
|
|
79
|
+
addr: int = 0,
|
|
80
|
+
description: str = "",
|
|
81
|
+
):
|
|
82
|
+
"""Add a driver or plugin parameter to the attribute list
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
name: The attribute name
|
|
86
|
+
param: The parameter string as seen in the INP link of the record
|
|
87
|
+
datatype: The datatype of the parameter
|
|
88
|
+
description: A description that appears in the HDF file as an attribute
|
|
89
|
+
"""
|
|
90
|
+
ET.SubElement(
|
|
91
|
+
self._root,
|
|
92
|
+
"Attribute",
|
|
93
|
+
name=name,
|
|
94
|
+
type="PARAM",
|
|
95
|
+
source=param,
|
|
96
|
+
addr=str(addr),
|
|
97
|
+
datatype=datatype.value,
|
|
98
|
+
description=description,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def __str__(self) -> str:
|
|
102
|
+
"""Output the XML pretty printed"""
|
|
103
|
+
ET.indent(self._root, space=" ", level=0)
|
|
104
|
+
return ET.tostring(self._root, xml_declaration=True, encoding="utf-8").decode()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def stop_busy_record(
|
|
108
|
+
signal: SignalRW[T],
|
|
109
|
+
value: T,
|
|
110
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
111
|
+
status_timeout: Optional[float] = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
await signal.set(value, wait=False, timeout=status_timeout)
|
|
114
|
+
await wait_for_value(signal, value, timeout=timeout)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Iterator, List
|
|
3
|
+
|
|
4
|
+
from event_model import StreamDatum, StreamResource, compose_stream_resource
|
|
5
|
+
|
|
6
|
+
from ophyd_async.core import DirectoryInfo
|
|
7
|
+
|
|
8
|
+
from ._hdfdataset import _HDFDataset
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _HDFFile:
|
|
12
|
+
"""
|
|
13
|
+
:param directory_info: Contains information about how to construct a StreamResource
|
|
14
|
+
:param full_file_name: Absolute path to the file to be written
|
|
15
|
+
:param datasets: Datasets to write into the file
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
directory_info: DirectoryInfo,
|
|
21
|
+
full_file_name: Path,
|
|
22
|
+
datasets: List[_HDFDataset],
|
|
23
|
+
) -> None:
|
|
24
|
+
self._last_emitted = 0
|
|
25
|
+
self._bundles = [
|
|
26
|
+
compose_stream_resource(
|
|
27
|
+
spec="AD_HDF5_SWMR_SLICE",
|
|
28
|
+
root=str(directory_info.root),
|
|
29
|
+
data_key=ds.name,
|
|
30
|
+
resource_path=str(full_file_name.relative_to(directory_info.root)),
|
|
31
|
+
resource_kwargs={
|
|
32
|
+
"path": ds.path,
|
|
33
|
+
"multiplier": ds.multiplier,
|
|
34
|
+
"timestamps": "/entry/instrument/NDAttributes/NDArrayTimeStamp",
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
for ds in datasets
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def stream_resources(self) -> Iterator[StreamResource]:
|
|
41
|
+
for bundle in self._bundles:
|
|
42
|
+
yield bundle.stream_resource_doc
|
|
43
|
+
|
|
44
|
+
def stream_data(self, indices_written: int) -> Iterator[StreamDatum]:
|
|
45
|
+
# Indices are relative to resource
|
|
46
|
+
if indices_written > self._last_emitted:
|
|
47
|
+
indices = dict(
|
|
48
|
+
start=self._last_emitted,
|
|
49
|
+
stop=indices_written,
|
|
50
|
+
)
|
|
51
|
+
self._last_emitted = indices_written
|
|
52
|
+
for bundle in self._bundles:
|
|
53
|
+
yield bundle.compose_stream_datum(indices)
|
|
54
|
+
return None
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import AsyncGenerator, AsyncIterator, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from bluesky.protocols import Descriptor, Hints, StreamAsset
|
|
6
|
+
|
|
7
|
+
from ophyd_async.core import (
|
|
8
|
+
DEFAULT_TIMEOUT,
|
|
9
|
+
AsyncStatus,
|
|
10
|
+
DetectorWriter,
|
|
11
|
+
DirectoryProvider,
|
|
12
|
+
NameProvider,
|
|
13
|
+
ShapeProvider,
|
|
14
|
+
set_and_wait_for_value,
|
|
15
|
+
wait_for_value,
|
|
16
|
+
)
|
|
17
|
+
from ophyd_async.core.signal import observe_value
|
|
18
|
+
|
|
19
|
+
from ._hdfdataset import _HDFDataset
|
|
20
|
+
from ._hdffile import _HDFFile
|
|
21
|
+
from .nd_file_hdf import FileWriteMode, NDFileHDF
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HDFWriter(DetectorWriter):
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
hdf: NDFileHDF,
|
|
28
|
+
directory_provider: DirectoryProvider,
|
|
29
|
+
name_provider: NameProvider,
|
|
30
|
+
shape_provider: ShapeProvider,
|
|
31
|
+
**scalar_datasets_paths: str,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.hdf = hdf
|
|
34
|
+
self._directory_provider = directory_provider
|
|
35
|
+
self._name_provider = name_provider
|
|
36
|
+
self._shape_provider = shape_provider
|
|
37
|
+
self._scalar_datasets_paths = scalar_datasets_paths
|
|
38
|
+
self._capture_status: Optional[AsyncStatus] = None
|
|
39
|
+
self._datasets: List[_HDFDataset] = []
|
|
40
|
+
self._file: Optional[_HDFFile] = None
|
|
41
|
+
self._multiplier = 1
|
|
42
|
+
|
|
43
|
+
async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]:
|
|
44
|
+
self._file = None
|
|
45
|
+
info = self._directory_provider()
|
|
46
|
+
await asyncio.gather(
|
|
47
|
+
self.hdf.num_extra_dims.set(0),
|
|
48
|
+
self.hdf.lazy_open.set(True),
|
|
49
|
+
self.hdf.swmr_mode.set(True),
|
|
50
|
+
# See https://github.com/bluesky/ophyd-async/issues/122
|
|
51
|
+
self.hdf.file_path.set(str(info.root / info.resource_dir)),
|
|
52
|
+
self.hdf.file_name.set(f"{info.prefix}{self.hdf.name}{info.suffix}"),
|
|
53
|
+
self.hdf.file_template.set("%s/%s.h5"),
|
|
54
|
+
self.hdf.file_write_mode.set(FileWriteMode.stream),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
assert (
|
|
58
|
+
await self.hdf.file_path_exists.get_value()
|
|
59
|
+
), f"File path {self.hdf.file_path.get_value()} for hdf plugin does not exist"
|
|
60
|
+
|
|
61
|
+
# Overwrite num_capture to go forever
|
|
62
|
+
await self.hdf.num_capture.set(0)
|
|
63
|
+
# Wait for it to start, stashing the status that tells us when it finishes
|
|
64
|
+
self._capture_status = await set_and_wait_for_value(self.hdf.capture, True)
|
|
65
|
+
name = self._name_provider()
|
|
66
|
+
detector_shape = tuple(await self._shape_provider())
|
|
67
|
+
self._multiplier = multiplier
|
|
68
|
+
outer_shape = (multiplier,) if multiplier > 1 else ()
|
|
69
|
+
# Add the main data
|
|
70
|
+
self._datasets = [
|
|
71
|
+
_HDFDataset(name, "/entry/data/data", detector_shape, multiplier)
|
|
72
|
+
]
|
|
73
|
+
# And all the scalar datasets
|
|
74
|
+
for ds_name, ds_path in self._scalar_datasets_paths.items():
|
|
75
|
+
self._datasets.append(
|
|
76
|
+
_HDFDataset(
|
|
77
|
+
f"{name}-{ds_name}",
|
|
78
|
+
f"/entry/instrument/NDAttributes/{ds_path}",
|
|
79
|
+
(),
|
|
80
|
+
multiplier,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
describe = {
|
|
84
|
+
ds.name: Descriptor(
|
|
85
|
+
source=self.hdf.full_file_name.source,
|
|
86
|
+
shape=outer_shape + tuple(ds.shape),
|
|
87
|
+
dtype="array" if ds.shape else "number",
|
|
88
|
+
external="STREAM:",
|
|
89
|
+
)
|
|
90
|
+
for ds in self._datasets
|
|
91
|
+
}
|
|
92
|
+
return describe
|
|
93
|
+
|
|
94
|
+
async def observe_indices_written(
|
|
95
|
+
self, timeout=DEFAULT_TIMEOUT
|
|
96
|
+
) -> AsyncGenerator[int, None]:
|
|
97
|
+
"""Wait until a specific index is ready to be collected"""
|
|
98
|
+
async for num_captured in observe_value(self.hdf.num_captured, timeout):
|
|
99
|
+
yield num_captured // self._multiplier
|
|
100
|
+
|
|
101
|
+
async def get_indices_written(self) -> int:
|
|
102
|
+
num_captured = await self.hdf.num_captured.get_value()
|
|
103
|
+
return num_captured // self._multiplier
|
|
104
|
+
|
|
105
|
+
async def collect_stream_docs(
|
|
106
|
+
self, indices_written: int
|
|
107
|
+
) -> AsyncIterator[StreamAsset]:
|
|
108
|
+
# TODO: fail if we get dropped frames
|
|
109
|
+
await self.hdf.flush_now.set(True)
|
|
110
|
+
if indices_written:
|
|
111
|
+
if not self._file:
|
|
112
|
+
self._file = _HDFFile(
|
|
113
|
+
self._directory_provider(),
|
|
114
|
+
# See https://github.com/bluesky/ophyd-async/issues/122
|
|
115
|
+
Path(await self.hdf.full_file_name.get_value()),
|
|
116
|
+
self._datasets,
|
|
117
|
+
)
|
|
118
|
+
for doc in self._file.stream_resources():
|
|
119
|
+
yield "stream_resource", doc
|
|
120
|
+
for doc in self._file.stream_data(indices_written):
|
|
121
|
+
yield "stream_datum", doc
|
|
122
|
+
|
|
123
|
+
async def close(self):
|
|
124
|
+
# Already done a caput callback in _capture_status, so can't do one here
|
|
125
|
+
await self.hdf.capture.set(0, wait=False)
|
|
126
|
+
await wait_for_value(self.hdf.capture, 0, DEFAULT_TIMEOUT)
|
|
127
|
+
if self._capture_status:
|
|
128
|
+
# We kicked off an open, so wait for it to return
|
|
129
|
+
await self._capture_status
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def hints(self) -> Hints:
|
|
133
|
+
return {"fields": [self._name_provider()]}
|
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
from
|
|
1
|
+
from enum import Enum
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
3
|
+
from ...signal.signal import epics_signal_rw
|
|
4
|
+
from ..utils import FileWriteMode, ad_r, ad_rw
|
|
5
|
+
from .nd_plugin import NDPluginBase
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
class
|
|
8
|
-
|
|
8
|
+
class Compression(str, Enum):
|
|
9
|
+
none = "None"
|
|
10
|
+
nbit = "N-bit"
|
|
11
|
+
szip = "szip"
|
|
12
|
+
zlib = "zlib"
|
|
13
|
+
blosc = "Blosc"
|
|
14
|
+
bslz4 = "BSLZ4"
|
|
15
|
+
lz4 = "LZ4"
|
|
16
|
+
jpeg = "JPEG"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NDFileHDF(NDPluginBase):
|
|
20
|
+
def __init__(self, prefix: str, name="") -> None:
|
|
9
21
|
# Define some signals
|
|
22
|
+
self.position_mode = ad_rw(bool, prefix + "PositionMode")
|
|
23
|
+
self.compression = ad_rw(Compression, prefix + "Compression")
|
|
24
|
+
self.num_extra_dims = ad_rw(int, prefix + "NumExtraDims")
|
|
10
25
|
self.file_path = ad_rw(str, prefix + "FilePath")
|
|
11
26
|
self.file_name = ad_rw(str, prefix + "FileName")
|
|
27
|
+
self.file_path_exists = ad_r(bool, prefix + "FilePathExists")
|
|
12
28
|
self.file_template = ad_rw(str, prefix + "FileTemplate")
|
|
13
29
|
self.full_file_name = ad_r(str, prefix + "FullFileName")
|
|
14
30
|
self.file_write_mode = ad_rw(FileWriteMode, prefix + "FileWriteMode")
|
|
@@ -20,3 +36,4 @@ class NDFileHDF(Device):
|
|
|
20
36
|
self.flush_now = epics_signal_rw(bool, prefix + "FlushNow")
|
|
21
37
|
self.array_size0 = ad_r(int, prefix + "ArraySize0")
|
|
22
38
|
self.array_size1 = ad_r(int, prefix + "ArraySize1")
|
|
39
|
+
super().__init__(prefix, name)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import Device
|
|
4
|
+
from ophyd_async.epics.signal import epics_signal_rw
|
|
5
|
+
|
|
6
|
+
from ..utils import ad_r, ad_rw
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Callback(str, Enum):
|
|
10
|
+
Enable = "Enable"
|
|
11
|
+
Disable = "Disable"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NDArrayBase(Device):
|
|
15
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
16
|
+
self.unique_id = ad_r(int, prefix + "UniqueId")
|
|
17
|
+
self.nd_attributes_file = epics_signal_rw(str, prefix + "NDAttributesFile")
|
|
18
|
+
super().__init__(name)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NDPluginBase(NDArrayBase):
|
|
22
|
+
def __init__(self, prefix: str, name: str = "") -> None:
|
|
23
|
+
self.nd_array_port = ad_rw(str, prefix + "NDArrayPort")
|
|
24
|
+
self.enable_callback = ad_rw(Callback, prefix + "EnableCallbacks")
|
|
25
|
+
self.nd_array_address = ad_rw(int, prefix + "NDArrayAddress")
|
|
26
|
+
super().__init__(prefix, name)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NDPluginStats(NDPluginBase):
|
|
30
|
+
pass
|
|
@@ -19,7 +19,7 @@ from ophyd_async.core import AsyncStatus, Device, StandardReadable, observe_valu
|
|
|
19
19
|
from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
class EnergyMode(Enum):
|
|
22
|
+
class EnergyMode(str, Enum):
|
|
23
23
|
"""Energy mode for `Sensor`"""
|
|
24
24
|
|
|
25
25
|
#: Low energy mode
|
|
@@ -112,7 +112,8 @@ class Mover(StandardReadable, Movable, Stoppable):
|
|
|
112
112
|
|
|
113
113
|
async def stop(self, success=True):
|
|
114
114
|
self._set_success = success
|
|
115
|
-
|
|
115
|
+
status = self.stop_.trigger()
|
|
116
|
+
await status
|
|
116
117
|
|
|
117
118
|
|
|
118
119
|
class SampleStage(Device):
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Sequence
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import DirectoryProvider, SignalR, StandardDetector
|
|
4
|
+
|
|
5
|
+
from ..areadetector.controllers import ADSimController
|
|
6
|
+
from ..areadetector.drivers import ADBase, ADBaseShapeProvider
|
|
7
|
+
from ..areadetector.writers import HDFWriter, NDFileHDF
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DemoADSimDetector(StandardDetector):
|
|
11
|
+
_controller: ADSimController
|
|
12
|
+
_writer: HDFWriter
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
drv: ADBase,
|
|
17
|
+
hdf: NDFileHDF,
|
|
18
|
+
directory_provider: DirectoryProvider,
|
|
19
|
+
name: str = "",
|
|
20
|
+
config_sigs: Sequence[SignalR] = (),
|
|
21
|
+
):
|
|
22
|
+
self.drv = drv
|
|
23
|
+
self.hdf = hdf
|
|
24
|
+
|
|
25
|
+
super().__init__(
|
|
26
|
+
ADSimController(self.drv),
|
|
27
|
+
HDFWriter(
|
|
28
|
+
self.hdf,
|
|
29
|
+
directory_provider,
|
|
30
|
+
lambda: self.name,
|
|
31
|
+
ADBaseShapeProvider(self.drv),
|
|
32
|
+
),
|
|
33
|
+
config_sigs=config_sigs,
|
|
34
|
+
name=name,
|
|
35
|
+
)
|
|
@@ -81,4 +81,5 @@ class Motor(StandardReadable, Movable, Stoppable):
|
|
|
81
81
|
self._set_success = success
|
|
82
82
|
# Put with completion will never complete as we are waiting for completion on
|
|
83
83
|
# the move above, so need to pass wait=False
|
|
84
|
-
|
|
84
|
+
status = self.stop_.trigger(wait=False)
|
|
85
|
+
await status
|
ophyd_async/epics/pvi.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from typing import Callable, Dict, FrozenSet, Optional, Type, TypedDict, TypeVar
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core.signal import Signal
|
|
4
|
+
from ophyd_async.core.signal_backend import SignalBackend
|
|
5
|
+
from ophyd_async.core.utils import DEFAULT_TIMEOUT
|
|
6
|
+
from ophyd_async.epics._backend._p4p import PvaSignalBackend
|
|
7
|
+
from ophyd_async.epics.signal.signal import (
|
|
8
|
+
epics_signal_r,
|
|
9
|
+
epics_signal_rw,
|
|
10
|
+
epics_signal_w,
|
|
11
|
+
epics_signal_x,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_pvi_mapping: Dict[FrozenSet[str], Callable[..., Signal]] = {
|
|
18
|
+
frozenset({"r", "w"}): lambda dtype, read_pv, write_pv: epics_signal_rw(
|
|
19
|
+
dtype, read_pv, write_pv
|
|
20
|
+
),
|
|
21
|
+
frozenset({"rw"}): lambda dtype, read_pv, write_pv: epics_signal_rw(
|
|
22
|
+
dtype, read_pv, write_pv
|
|
23
|
+
),
|
|
24
|
+
frozenset({"r"}): lambda dtype, read_pv, _: epics_signal_r(dtype, read_pv),
|
|
25
|
+
frozenset({"w"}): lambda dtype, _, write_pv: epics_signal_w(dtype, write_pv),
|
|
26
|
+
frozenset({"x"}): lambda _, __, write_pv: epics_signal_x(write_pv),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PVIEntry(TypedDict, total=False):
|
|
31
|
+
d: str
|
|
32
|
+
r: str
|
|
33
|
+
rw: str
|
|
34
|
+
w: str
|
|
35
|
+
x: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def pvi_get(
|
|
39
|
+
read_pv: str, timeout: float = DEFAULT_TIMEOUT
|
|
40
|
+
) -> Dict[str, PVIEntry]:
|
|
41
|
+
"""Makes a PvaSignalBackend purely to connect to PVI information.
|
|
42
|
+
|
|
43
|
+
This backend is simply thrown away at the end of this method. This is useful
|
|
44
|
+
because the backend handles a CancelledError exception that gets thrown on
|
|
45
|
+
timeout, and therefore can be used for error reporting."""
|
|
46
|
+
backend: SignalBackend = PvaSignalBackend(None, read_pv, read_pv)
|
|
47
|
+
await backend.connect(timeout=timeout)
|
|
48
|
+
d: Dict[str, Dict[str, Dict[str, str]]] = await backend.get_value()
|
|
49
|
+
pv_info = d.get("pvi") or {}
|
|
50
|
+
result = {}
|
|
51
|
+
|
|
52
|
+
for attr_name, attr_info in pv_info.items():
|
|
53
|
+
result[attr_name] = PVIEntry(**attr_info) # type: ignore
|
|
54
|
+
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def make_signal(signal_pvi: PVIEntry, dtype: Optional[Type[T]] = None) -> Signal[T]:
|
|
59
|
+
"""Make a signal.
|
|
60
|
+
|
|
61
|
+
This assumes datatype is None so it can be used to create dynamic signals.
|
|
62
|
+
"""
|
|
63
|
+
operations = frozenset(signal_pvi.keys())
|
|
64
|
+
pvs = [signal_pvi[i] for i in operations] # type: ignore
|
|
65
|
+
signal_factory = _pvi_mapping[operations]
|
|
66
|
+
|
|
67
|
+
write_pv = "pva://" + pvs[0]
|
|
68
|
+
read_pv = write_pv if len(pvs) < 2 else "pva://" + pvs[1]
|
|
69
|
+
|
|
70
|
+
return signal_factory(dtype, read_pv, write_pv)
|