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.
Files changed (60) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +47 -12
  3. ophyd_async/core/_providers.py +66 -0
  4. ophyd_async/core/async_status.py +7 -5
  5. ophyd_async/core/detector.py +321 -0
  6. ophyd_async/core/device.py +184 -0
  7. ophyd_async/core/device_save_loader.py +286 -0
  8. ophyd_async/core/flyer.py +94 -0
  9. ophyd_async/core/{_device/_signal/signal.py → signal.py} +46 -18
  10. ophyd_async/core/{_device/_backend/signal_backend.py → signal_backend.py} +6 -2
  11. ophyd_async/core/{_device/_backend/sim_signal_backend.py → sim_signal_backend.py} +6 -2
  12. ophyd_async/core/{_device/standard_readable.py → standard_readable.py} +3 -3
  13. ophyd_async/core/utils.py +79 -29
  14. ophyd_async/epics/_backend/_aioca.py +38 -25
  15. ophyd_async/epics/_backend/_p4p.py +62 -27
  16. ophyd_async/epics/_backend/common.py +20 -0
  17. ophyd_async/epics/areadetector/__init__.py +10 -13
  18. ophyd_async/epics/areadetector/controllers/__init__.py +4 -0
  19. ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +52 -0
  20. ophyd_async/epics/areadetector/controllers/pilatus_controller.py +49 -0
  21. ophyd_async/epics/areadetector/drivers/__init__.py +15 -0
  22. ophyd_async/epics/areadetector/drivers/ad_base.py +111 -0
  23. ophyd_async/epics/areadetector/drivers/pilatus_driver.py +18 -0
  24. ophyd_async/epics/areadetector/single_trigger_det.py +4 -4
  25. ophyd_async/epics/areadetector/utils.py +91 -3
  26. ophyd_async/epics/areadetector/writers/__init__.py +5 -0
  27. ophyd_async/epics/areadetector/writers/_hdfdataset.py +10 -0
  28. ophyd_async/epics/areadetector/writers/_hdffile.py +54 -0
  29. ophyd_async/epics/areadetector/writers/hdf_writer.py +133 -0
  30. ophyd_async/epics/areadetector/{nd_file_hdf.py → writers/nd_file_hdf.py} +22 -5
  31. ophyd_async/epics/areadetector/writers/nd_plugin.py +30 -0
  32. ophyd_async/epics/demo/__init__.py +3 -2
  33. ophyd_async/epics/demo/demo_ad_sim_detector.py +35 -0
  34. ophyd_async/epics/motion/motor.py +2 -1
  35. ophyd_async/epics/pvi.py +70 -0
  36. ophyd_async/epics/signal/__init__.py +0 -2
  37. ophyd_async/epics/signal/signal.py +1 -1
  38. ophyd_async/panda/__init__.py +12 -8
  39. ophyd_async/panda/panda.py +43 -134
  40. ophyd_async/panda/panda_controller.py +41 -0
  41. ophyd_async/panda/table.py +158 -0
  42. ophyd_async/panda/utils.py +15 -0
  43. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/METADATA +49 -42
  44. ophyd_async-0.3a1.dist-info/RECORD +56 -0
  45. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/WHEEL +1 -1
  46. ophyd_async/core/_device/__init__.py +0 -0
  47. ophyd_async/core/_device/_backend/__init__.py +0 -0
  48. ophyd_async/core/_device/_signal/__init__.py +0 -0
  49. ophyd_async/core/_device/device.py +0 -60
  50. ophyd_async/core/_device/device_collector.py +0 -121
  51. ophyd_async/core/_device/device_vector.py +0 -14
  52. ophyd_async/epics/areadetector/ad_driver.py +0 -18
  53. ophyd_async/epics/areadetector/directory_provider.py +0 -18
  54. ophyd_async/epics/areadetector/hdf_streamer_det.py +0 -167
  55. ophyd_async/epics/areadetector/nd_plugin.py +0 -13
  56. ophyd_async/epics/signal/pvi_get.py +0 -22
  57. ophyd_async-0.1.0.dist-info/RECORD +0 -45
  58. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/LICENSE +0 -0
  59. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/entry_points.txt +0 -0
  60. {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 .ad_driver import ADDriver
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: ADDriver,
16
+ drv: ADBase,
17
17
  read_uncached: Sequence[SignalR] = (),
18
18
  name="",
19
- **plugins: NDPlugin,
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,5 @@
1
+ from .hdf_writer import HDFWriter
2
+ from .nd_file_hdf import NDFileHDF
3
+ from .nd_plugin import NDPluginBase, NDPluginStats
4
+
5
+ __all__ = ["HDFWriter", "NDFileHDF", "NDPluginBase", "NDPluginStats"]
@@ -0,0 +1,10 @@
1
+ from dataclasses import dataclass
2
+ from typing import Sequence
3
+
4
+
5
+ @dataclass
6
+ class _HDFDataset:
7
+ name: str
8
+ path: str
9
+ shape: Sequence[int]
10
+ multiplier: int
@@ -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 ophyd_async.core import Device
1
+ from enum import Enum
2
2
 
3
- from ..signal.signal import epics_signal_rw
4
- from .utils import FileWriteMode, ad_r, ad_rw
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 NDFileHDF(Device):
8
- def __init__(self, prefix: str) -> None:
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
- await self.stop_.execute()
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
- await self.stop_.execute(wait=False)
84
+ status = self.stop_.trigger(wait=False)
85
+ await status
@@ -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)
@@ -1,8 +1,6 @@
1
- from .pvi_get import pvi_get
2
1
  from .signal import epics_signal_r, epics_signal_rw, epics_signal_w, epics_signal_x
3
2
 
4
3
  __all__ = [
5
- "pvi_get",
6
4
  "epics_signal_r",
7
5
  "epics_signal_rw",
8
6
  "epics_signal_w",