ophyd-async 0.1.0__py3-none-any.whl → 0.3.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 (94) hide show
  1. ophyd_async/__init__.py +1 -4
  2. ophyd_async/_version.py +2 -2
  3. ophyd_async/core/__init__.py +91 -19
  4. ophyd_async/core/_providers.py +68 -0
  5. ophyd_async/core/async_status.py +90 -42
  6. ophyd_async/core/detector.py +341 -0
  7. ophyd_async/core/device.py +226 -0
  8. ophyd_async/core/device_save_loader.py +286 -0
  9. ophyd_async/core/flyer.py +85 -0
  10. ophyd_async/core/mock_signal_backend.py +82 -0
  11. ophyd_async/core/mock_signal_utils.py +145 -0
  12. ophyd_async/core/{_device/_signal/signal.py → signal.py} +249 -61
  13. ophyd_async/core/{_device/_backend/signal_backend.py → signal_backend.py} +12 -5
  14. ophyd_async/core/{_device/_backend/sim_signal_backend.py → soft_signal_backend.py} +54 -48
  15. ophyd_async/core/standard_readable.py +261 -0
  16. ophyd_async/core/utils.py +127 -30
  17. ophyd_async/epics/_backend/_aioca.py +62 -43
  18. ophyd_async/epics/_backend/_p4p.py +100 -52
  19. ophyd_async/epics/_backend/common.py +25 -0
  20. ophyd_async/epics/areadetector/__init__.py +16 -15
  21. ophyd_async/epics/areadetector/aravis.py +63 -0
  22. ophyd_async/epics/areadetector/controllers/__init__.py +5 -0
  23. ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +52 -0
  24. ophyd_async/epics/areadetector/controllers/aravis_controller.py +78 -0
  25. ophyd_async/epics/areadetector/controllers/kinetix_controller.py +49 -0
  26. ophyd_async/epics/areadetector/controllers/pilatus_controller.py +61 -0
  27. ophyd_async/epics/areadetector/controllers/vimba_controller.py +66 -0
  28. ophyd_async/epics/areadetector/drivers/__init__.py +21 -0
  29. ophyd_async/epics/areadetector/drivers/ad_base.py +107 -0
  30. ophyd_async/epics/areadetector/drivers/aravis_driver.py +38 -0
  31. ophyd_async/epics/areadetector/drivers/kinetix_driver.py +27 -0
  32. ophyd_async/epics/areadetector/drivers/pilatus_driver.py +21 -0
  33. ophyd_async/epics/areadetector/drivers/vimba_driver.py +63 -0
  34. ophyd_async/epics/areadetector/kinetix.py +46 -0
  35. ophyd_async/epics/areadetector/pilatus.py +45 -0
  36. ophyd_async/epics/areadetector/single_trigger_det.py +18 -10
  37. ophyd_async/epics/areadetector/utils.py +91 -13
  38. ophyd_async/epics/areadetector/vimba.py +43 -0
  39. ophyd_async/epics/areadetector/writers/__init__.py +5 -0
  40. ophyd_async/epics/areadetector/writers/_hdfdataset.py +10 -0
  41. ophyd_async/epics/areadetector/writers/_hdffile.py +54 -0
  42. ophyd_async/epics/areadetector/writers/hdf_writer.py +142 -0
  43. ophyd_async/epics/areadetector/writers/nd_file_hdf.py +40 -0
  44. ophyd_async/epics/areadetector/writers/nd_plugin.py +38 -0
  45. ophyd_async/epics/demo/__init__.py +78 -51
  46. ophyd_async/epics/demo/demo_ad_sim_detector.py +35 -0
  47. ophyd_async/epics/motion/motor.py +67 -52
  48. ophyd_async/epics/pvi/__init__.py +3 -0
  49. ophyd_async/epics/pvi/pvi.py +318 -0
  50. ophyd_async/epics/signal/__init__.py +8 -3
  51. ophyd_async/epics/signal/signal.py +27 -10
  52. ophyd_async/log.py +130 -0
  53. ophyd_async/panda/__init__.py +24 -7
  54. ophyd_async/panda/_common_blocks.py +49 -0
  55. ophyd_async/panda/_hdf_panda.py +48 -0
  56. ophyd_async/panda/_panda_controller.py +37 -0
  57. ophyd_async/panda/_table.py +158 -0
  58. ophyd_async/panda/_trigger.py +39 -0
  59. ophyd_async/panda/_utils.py +15 -0
  60. ophyd_async/panda/writers/__init__.py +3 -0
  61. ophyd_async/panda/writers/_hdf_writer.py +220 -0
  62. ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
  63. ophyd_async/plan_stubs/__init__.py +13 -0
  64. ophyd_async/plan_stubs/ensure_connected.py +22 -0
  65. ophyd_async/plan_stubs/fly.py +149 -0
  66. ophyd_async/protocols.py +126 -0
  67. ophyd_async/sim/__init__.py +11 -0
  68. ophyd_async/sim/demo/__init__.py +3 -0
  69. ophyd_async/sim/demo/sim_motor.py +103 -0
  70. ophyd_async/sim/pattern_generator.py +318 -0
  71. ophyd_async/sim/sim_pattern_detector_control.py +55 -0
  72. ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
  73. ophyd_async/sim/sim_pattern_generator.py +37 -0
  74. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/METADATA +35 -67
  75. ophyd_async-0.3.0.dist-info/RECORD +86 -0
  76. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/WHEEL +1 -1
  77. ophyd_async/core/_device/__init__.py +0 -0
  78. ophyd_async/core/_device/_backend/__init__.py +0 -0
  79. ophyd_async/core/_device/_signal/__init__.py +0 -0
  80. ophyd_async/core/_device/device.py +0 -60
  81. ophyd_async/core/_device/device_collector.py +0 -121
  82. ophyd_async/core/_device/device_vector.py +0 -14
  83. ophyd_async/core/_device/standard_readable.py +0 -72
  84. ophyd_async/epics/areadetector/ad_driver.py +0 -18
  85. ophyd_async/epics/areadetector/directory_provider.py +0 -18
  86. ophyd_async/epics/areadetector/hdf_streamer_det.py +0 -167
  87. ophyd_async/epics/areadetector/nd_file_hdf.py +0 -22
  88. ophyd_async/epics/areadetector/nd_plugin.py +0 -13
  89. ophyd_async/epics/signal/pvi_get.py +0 -22
  90. ophyd_async/panda/panda.py +0 -332
  91. ophyd_async-0.1.0.dist-info/RECORD +0 -45
  92. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/LICENSE +0 -0
  93. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/entry_points.txt +0 -0
  94. {ophyd_async-0.1.0.dist-info → ophyd_async-0.3.0.dist-info}/top_level.txt +0 -0
@@ -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 = {
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,142 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+ from typing import AsyncGenerator, AsyncIterator, Dict, List, Optional
4
+
5
+ from bluesky.protocols import DataKey, 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, DataKey]:
44
+ self._file = None
45
+ info = self._directory_provider()
46
+ file_path = str(info.root / info.resource_dir)
47
+ await asyncio.gather(
48
+ self.hdf.num_extra_dims.set(0),
49
+ self.hdf.lazy_open.set(True),
50
+ self.hdf.swmr_mode.set(True),
51
+ # See https://github.com/bluesky/ophyd-async/issues/122
52
+ self.hdf.file_path.set(file_path),
53
+ self.hdf.file_name.set(f"{info.prefix}{self.hdf.name}{info.suffix}"),
54
+ self.hdf.file_template.set("%s/%s.h5"),
55
+ self.hdf.file_write_mode.set(FileWriteMode.stream),
56
+ # Never use custom xml layout file but use the one defined
57
+ # in the source code file NDFileHDF5LayoutXML.cpp
58
+ self.hdf.xml_file_name.set(""),
59
+ )
60
+
61
+ assert (
62
+ await self.hdf.file_path_exists.get_value()
63
+ ), f"File path {file_path} for hdf plugin does not exist"
64
+
65
+ # Overwrite num_capture to go forever
66
+ await self.hdf.num_capture.set(0)
67
+ # Wait for it to start, stashing the status that tells us when it finishes
68
+ self._capture_status = await set_and_wait_for_value(self.hdf.capture, True)
69
+ name = self._name_provider()
70
+ detector_shape = tuple(await self._shape_provider())
71
+ self._multiplier = multiplier
72
+ outer_shape = (multiplier,) if multiplier > 1 else ()
73
+ # Add the main data
74
+ self._datasets = [
75
+ _HDFDataset(name, "/entry/data/data", detector_shape, multiplier)
76
+ ]
77
+ # And all the scalar datasets
78
+ for ds_name, ds_path in self._scalar_datasets_paths.items():
79
+ self._datasets.append(
80
+ _HDFDataset(
81
+ f"{name}-{ds_name}",
82
+ f"/entry/instrument/NDAttributes/{ds_path}",
83
+ (),
84
+ multiplier,
85
+ )
86
+ )
87
+ describe = {
88
+ ds.name: DataKey(
89
+ source=self.hdf.full_file_name.source,
90
+ shape=outer_shape + tuple(ds.shape),
91
+ dtype="array" if ds.shape else "number",
92
+ external="STREAM:",
93
+ )
94
+ for ds in self._datasets
95
+ }
96
+ return describe
97
+
98
+ async def observe_indices_written(
99
+ self, timeout=DEFAULT_TIMEOUT
100
+ ) -> AsyncGenerator[int, None]:
101
+ """Wait until a specific index is ready to be collected"""
102
+ async for num_captured in observe_value(self.hdf.num_captured, timeout):
103
+ yield num_captured // self._multiplier
104
+
105
+ async def get_indices_written(self) -> int:
106
+ num_captured = await self.hdf.num_captured.get_value()
107
+ return num_captured // self._multiplier
108
+
109
+ async def collect_stream_docs(
110
+ self, indices_written: int
111
+ ) -> AsyncIterator[StreamAsset]:
112
+ # TODO: fail if we get dropped frames
113
+ await self.hdf.flush_now.set(True)
114
+ if indices_written:
115
+ if not self._file:
116
+ path = Path(await self.hdf.full_file_name.get_value())
117
+ self._file = _HDFFile(
118
+ self._directory_provider(),
119
+ # See https://github.com/bluesky/ophyd-async/issues/122
120
+ path,
121
+ self._datasets,
122
+ )
123
+ # stream resource says "here is a dataset",
124
+ # stream datum says "here are N frames in that stream resource",
125
+ # you get one stream resource and many stream datums per scan
126
+
127
+ for doc in self._file.stream_resources():
128
+ yield "stream_resource", doc
129
+ for doc in self._file.stream_data(indices_written):
130
+ yield "stream_datum", doc
131
+
132
+ async def close(self):
133
+ # Already done a caput callback in _capture_status, so can't do one here
134
+ await self.hdf.capture.set(0, wait=False)
135
+ await wait_for_value(self.hdf.capture, 0, DEFAULT_TIMEOUT)
136
+ if self._capture_status:
137
+ # We kicked off an open, so wait for it to return
138
+ await self._capture_status
139
+
140
+ @property
141
+ def hints(self) -> Hints:
142
+ return {"fields": [self._name_provider()]}
@@ -0,0 +1,40 @@
1
+ from enum import Enum
2
+
3
+ from ...signal.signal import epics_signal_r, epics_signal_rw, epics_signal_rw_rbv
4
+ from ..utils import FileWriteMode
5
+ from .nd_plugin import NDPluginBase
6
+
7
+
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:
21
+ # Define some signals
22
+ self.position_mode = epics_signal_rw_rbv(bool, prefix + "PositionMode")
23
+ self.compression = epics_signal_rw_rbv(Compression, prefix + "Compression")
24
+ self.num_extra_dims = epics_signal_rw_rbv(int, prefix + "NumExtraDims")
25
+ self.file_path = epics_signal_rw_rbv(str, prefix + "FilePath")
26
+ self.file_name = epics_signal_rw_rbv(str, prefix + "FileName")
27
+ self.file_path_exists = epics_signal_r(bool, prefix + "FilePathExists_RBV")
28
+ self.file_template = epics_signal_rw_rbv(str, prefix + "FileTemplate")
29
+ self.full_file_name = epics_signal_r(str, prefix + "FullFileName_RBV")
30
+ self.file_write_mode = epics_signal_rw_rbv(
31
+ FileWriteMode, prefix + "FileWriteMode"
32
+ )
33
+ self.num_capture = epics_signal_rw_rbv(int, prefix + "NumCapture")
34
+ self.num_captured = epics_signal_r(int, prefix + "NumCaptured_RBV")
35
+ self.swmr_mode = epics_signal_rw_rbv(bool, prefix + "SWMRMode")
36
+ self.lazy_open = epics_signal_rw_rbv(bool, prefix + "LazyOpen")
37
+ self.capture = epics_signal_rw_rbv(bool, prefix + "Capture")
38
+ self.flush_now = epics_signal_rw(bool, prefix + "FlushNow")
39
+ self.xml_file_name = epics_signal_rw_rbv(str, prefix + "XMLFileName")
40
+ super().__init__(prefix, name)
@@ -0,0 +1,38 @@
1
+ from enum import Enum
2
+
3
+ from ophyd_async.core import Device
4
+ from ophyd_async.epics.signal import epics_signal_rw
5
+ from ophyd_async.epics.signal.signal import epics_signal_r, epics_signal_rw_rbv
6
+
7
+
8
+ class Callback(str, Enum):
9
+ Enable = "Enable"
10
+ Disable = "Disable"
11
+
12
+
13
+ class NDArrayBase(Device):
14
+ def __init__(self, prefix: str, name: str = "") -> None:
15
+ self.unique_id = epics_signal_r(int, prefix + "UniqueId_RBV")
16
+ self.nd_attributes_file = epics_signal_rw(str, prefix + "NDAttributesFile")
17
+ self.acquire = epics_signal_rw_rbv(bool, prefix + "Acquire")
18
+ self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV")
19
+ self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV")
20
+ self.array_counter = epics_signal_rw_rbv(int, prefix + "ArrayCounter")
21
+ # There is no _RBV for this one
22
+ self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins")
23
+
24
+ super().__init__(name=name)
25
+
26
+
27
+ class NDPluginBase(NDArrayBase):
28
+ def __init__(self, prefix: str, name: str = "") -> None:
29
+ self.nd_array_port = epics_signal_rw_rbv(str, prefix + "NDArrayPort")
30
+ self.enable_callback = epics_signal_rw_rbv(Callback, prefix + "EnableCallbacks")
31
+ self.nd_array_address = epics_signal_rw_rbv(int, prefix + "NDArrayAddress")
32
+ self.array_size0 = epics_signal_r(int, prefix + "ArraySize0_RBV")
33
+ self.array_size1 = epics_signal_r(int, prefix + "ArraySize1_RBV")
34
+ super().__init__(prefix, name)
35
+
36
+
37
+ class NDPluginStats(NDPluginBase):
38
+ pass
@@ -6,20 +6,33 @@ import random
6
6
  import string
7
7
  import subprocess
8
8
  import sys
9
- import time
10
9
  from enum import Enum
11
10
  from pathlib import Path
12
- from typing import Callable, List, Optional
13
11
 
14
12
  import numpy as np
15
13
  from bluesky.protocols import Movable, Stoppable
16
14
 
17
- from ophyd_async.core import AsyncStatus, Device, StandardReadable, observe_value
15
+ from ophyd_async.core import (
16
+ ConfigSignal,
17
+ Device,
18
+ DeviceVector,
19
+ HintedSignal,
20
+ StandardReadable,
21
+ WatchableAsyncStatus,
22
+ observe_value,
23
+ )
24
+ from ophyd_async.core.async_status import AsyncStatus
25
+ from ophyd_async.core.utils import (
26
+ DEFAULT_TIMEOUT,
27
+ CalculatableTimeout,
28
+ CalculateTimeout,
29
+ WatcherUpdate,
30
+ )
18
31
 
19
32
  from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x
20
33
 
21
34
 
22
- class EnergyMode(Enum):
35
+ class EnergyMode(str, Enum):
23
36
  """Energy mode for `Sensor`"""
24
37
 
25
38
  #: Low energy mode
@@ -33,35 +46,41 @@ class Sensor(StandardReadable):
33
46
 
34
47
  def __init__(self, prefix: str, name="") -> None:
35
48
  # Define some signals
36
- self.value = epics_signal_r(float, prefix + "Value")
37
- self.mode = epics_signal_rw(EnergyMode, prefix + "Mode")
38
- # Set name and signals for read() and read_configuration()
39
- self.set_readable_signals(
40
- read=[self.value],
41
- config=[self.mode],
42
- )
49
+ with self.add_children_as_readables(HintedSignal):
50
+ self.value = epics_signal_r(float, prefix + "Value")
51
+ with self.add_children_as_readables(ConfigSignal):
52
+ self.mode = epics_signal_rw(EnergyMode, prefix + "Mode")
53
+
43
54
  super().__init__(name=name)
44
55
 
45
56
 
57
+ class SensorGroup(StandardReadable):
58
+ def __init__(self, prefix: str, name: str = "", sensor_count: int = 3) -> None:
59
+ with self.add_children_as_readables():
60
+ self.sensors = DeviceVector(
61
+ {i: Sensor(f"{prefix}{i}:") for i in range(1, sensor_count + 1)}
62
+ )
63
+
64
+ super().__init__(name)
65
+
66
+
46
67
  class Mover(StandardReadable, Movable, Stoppable):
47
68
  """A demo movable that moves based on velocity"""
48
69
 
49
70
  def __init__(self, prefix: str, name="") -> None:
50
71
  # Define some signals
72
+ with self.add_children_as_readables(HintedSignal):
73
+ self.readback = epics_signal_r(float, prefix + "Readback")
74
+ with self.add_children_as_readables(ConfigSignal):
75
+ self.velocity = epics_signal_rw(float, prefix + "Velocity")
76
+ self.units = epics_signal_r(str, prefix + "Readback.EGU")
51
77
  self.setpoint = epics_signal_rw(float, prefix + "Setpoint")
52
- self.readback = epics_signal_r(float, prefix + "Readback")
53
- self.velocity = epics_signal_rw(float, prefix + "Velocity")
54
- self.units = epics_signal_r(str, prefix + "Readback.EGU")
55
78
  self.precision = epics_signal_r(int, prefix + "Readback.PREC")
56
79
  # Signals that collide with standard methods should have a trailing underscore
57
80
  self.stop_ = epics_signal_x(prefix + "Stop.PROC")
58
81
  # Whether set() should complete successfully or not
59
82
  self._set_success = True
60
- # Set name and signals for read() and read_configuration()
61
- self.set_readable_signals(
62
- read=[self.readback],
63
- config=[self.velocity, self.units],
64
- )
83
+
65
84
  super().__init__(name=name)
66
85
 
67
86
  def set_name(self, name: str):
@@ -69,50 +88,47 @@ class Mover(StandardReadable, Movable, Stoppable):
69
88
  # Readback should be named the same as its parent in read()
70
89
  self.readback.set_name(name)
71
90
 
72
- async def _move(self, new_position: float, watchers: List[Callable] = []):
91
+ @WatchableAsyncStatus.wrap
92
+ async def set(
93
+ self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
94
+ ):
73
95
  self._set_success = True
74
- # time.monotonic won't go backwards in case of NTP corrections
75
- start = time.monotonic()
76
- old_position, units, precision = await asyncio.gather(
96
+ old_position, units, precision, velocity = await asyncio.gather(
77
97
  self.setpoint.get_value(),
78
98
  self.units.get_value(),
79
99
  self.precision.get_value(),
100
+ self.velocity.get_value(),
80
101
  )
102
+ if timeout is CalculateTimeout:
103
+ assert velocity > 0, "Mover has zero velocity"
104
+ timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT
105
+ # Make an Event that will be set on completion, and a Status that will
106
+ # error if not done in time
107
+ done = asyncio.Event()
108
+ done_status = AsyncStatus(asyncio.wait_for(done.wait(), timeout))
81
109
  # Wait for the value to set, but don't wait for put completion callback
82
110
  await self.setpoint.set(new_position, wait=False)
83
- async for current_position in observe_value(self.readback):
84
- for watcher in watchers:
85
- watcher(
86
- name=self.name,
87
- current=current_position,
88
- initial=old_position,
89
- target=new_position,
90
- unit=units,
91
- precision=precision,
92
- time_elapsed=time.monotonic() - start,
93
- )
111
+ async for current_position in observe_value(
112
+ self.readback, done_status=done_status
113
+ ):
114
+ yield WatcherUpdate(
115
+ current=current_position,
116
+ initial=old_position,
117
+ target=new_position,
118
+ name=self.name,
119
+ unit=units,
120
+ precision=precision,
121
+ )
94
122
  if np.isclose(current_position, new_position):
123
+ done.set()
95
124
  break
96
125
  if not self._set_success:
97
126
  raise RuntimeError("Motor was stopped")
98
127
 
99
- def move(self, new_position: float, timeout: Optional[float] = None):
100
- """Commandline only synchronous move of a Motor"""
101
- from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
102
-
103
- if in_bluesky_event_loop():
104
- raise RuntimeError("Will deadlock run engine if run in a plan")
105
- call_in_bluesky_event_loop(self._move(new_position), timeout) # type: ignore
106
-
107
- # TODO: this fails if we call from the cli, but works if we "ipython await" it
108
- def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus:
109
- watchers: List[Callable] = []
110
- coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout)
111
- return AsyncStatus(coro, watchers)
112
-
113
128
  async def stop(self, success=True):
114
129
  self._set_success = success
115
- await self.stop_.execute()
130
+ status = self.stop_.trigger()
131
+ await status
116
132
 
117
133
 
118
134
  class SampleStage(Device):
@@ -134,11 +150,22 @@ def start_ioc_subprocess() -> str:
134
150
  pv_prefix = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) + ":"
135
151
  here = Path(__file__).absolute().parent
136
152
  args = [sys.executable, "-m", "epicscorelibs.ioc"]
153
+
154
+ # Create standalone sensor
137
155
  args += ["-m", f"P={pv_prefix}"]
138
156
  args += ["-d", str(here / "sensor.db")]
139
- for suff in "XY":
140
- args += ["-m", f"P={pv_prefix}{suff}:"]
157
+
158
+ # Create sensor group
159
+ for suffix in ["1", "2", "3"]:
160
+ args += ["-m", f"P={pv_prefix}{suffix}:"]
161
+ args += ["-d", str(here / "sensor.db")]
162
+
163
+ # Create X and Y motors
164
+ for suffix in ["X", "Y"]:
165
+ args += ["-m", f"P={pv_prefix}{suffix}:"]
141
166
  args += ["-d", str(here / "mover.db")]
167
+
168
+ # Start IOC
142
169
  process = subprocess.Popen(
143
170
  args,
144
171
  stdin=subprocess.PIPE,
@@ -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
+ )
@@ -1,10 +1,20 @@
1
1
  import asyncio
2
- import time
3
- from typing import Callable, List, Optional
4
2
 
5
3
  from bluesky.protocols import Movable, Stoppable
6
4
 
7
- from ophyd_async.core import AsyncStatus, StandardReadable
5
+ from ophyd_async.core import (
6
+ ConfigSignal,
7
+ HintedSignal,
8
+ StandardReadable,
9
+ WatchableAsyncStatus,
10
+ )
11
+ from ophyd_async.core.signal import observe_value
12
+ from ophyd_async.core.utils import (
13
+ DEFAULT_TIMEOUT,
14
+ CalculatableTimeout,
15
+ CalculateTimeout,
16
+ WatcherUpdate,
17
+ )
8
18
 
9
19
  from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x
10
20
 
@@ -14,71 +24,76 @@ class Motor(StandardReadable, Movable, Stoppable):
14
24
 
15
25
  def __init__(self, prefix: str, name="") -> None:
16
26
  # Define some signals
17
- self.setpoint = epics_signal_rw(float, prefix + ".VAL")
18
- self.readback = epics_signal_r(float, prefix + ".RBV")
19
- self.velocity = epics_signal_rw(float, prefix + ".VELO")
20
- self.units = epics_signal_r(str, prefix + ".EGU")
27
+ with self.add_children_as_readables(ConfigSignal):
28
+ self.motor_egu = epics_signal_r(str, prefix + ".EGU")
29
+ self.velocity = epics_signal_rw(float, prefix + ".VELO")
30
+
31
+ with self.add_children_as_readables(HintedSignal):
32
+ self.user_readback = epics_signal_r(float, prefix + ".RBV")
33
+
34
+ self.user_setpoint = epics_signal_rw(float, prefix + ".VAL")
35
+ self.max_velocity = epics_signal_r(float, prefix + ".VMAX")
36
+ self.acceleration_time = epics_signal_rw(float, prefix + ".ACCL")
21
37
  self.precision = epics_signal_r(int, prefix + ".PREC")
22
- # Signals that collide with standard methods should have a trailing underscore
23
- self.stop_ = epics_signal_x(prefix + ".STOP")
38
+ self.deadband = epics_signal_r(float, prefix + ".RDBD")
39
+ self.motor_done_move = epics_signal_r(int, prefix + ".DMOV")
40
+ self.low_limit_travel = epics_signal_rw(float, prefix + ".LLM")
41
+ self.high_limit_travel = epics_signal_rw(float, prefix + ".HLM")
42
+
43
+ self.motor_stop = epics_signal_x(prefix + ".STOP")
24
44
  # Whether set() should complete successfully or not
25
45
  self._set_success = True
26
- # Set name and signals for read() and read_configuration()
27
- self.set_readable_signals(
28
- read=[self.readback],
29
- config=[self.velocity, self.units],
30
- )
31
46
  super().__init__(name=name)
32
47
 
33
48
  def set_name(self, name: str):
34
49
  super().set_name(name)
35
50
  # Readback should be named the same as its parent in read()
36
- self.readback.set_name(name)
51
+ self.user_readback.set_name(name)
37
52
 
38
- async def _move(self, new_position: float, watchers: List[Callable] = []):
53
+ @WatchableAsyncStatus.wrap
54
+ async def set(
55
+ self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
56
+ ):
39
57
  self._set_success = True
40
- start = time.monotonic()
41
- old_position, units, precision = await asyncio.gather(
42
- self.setpoint.get_value(),
43
- self.units.get_value(),
58
+ (
59
+ old_position,
60
+ units,
61
+ precision,
62
+ velocity,
63
+ acceleration_time,
64
+ ) = await asyncio.gather(
65
+ self.user_setpoint.get_value(),
66
+ self.motor_egu.get_value(),
44
67
  self.precision.get_value(),
68
+ self.velocity.get_value(),
69
+ self.acceleration_time.get_value(),
45
70
  )
46
-
47
- def update_watchers(current_position: float):
48
- for watcher in watchers:
49
- watcher(
50
- name=self.name,
51
- current=current_position,
52
- initial=old_position,
53
- target=new_position,
54
- unit=units,
55
- precision=precision,
56
- time_elapsed=time.monotonic() - start,
57
- )
58
-
59
- self.readback.subscribe_value(update_watchers)
60
- try:
61
- await self.setpoint.set(new_position)
62
- finally:
63
- self.readback.clear_sub(update_watchers)
71
+ if timeout is CalculateTimeout:
72
+ assert velocity > 0, "Motor has zero velocity"
73
+ timeout = (
74
+ abs(new_position - old_position) / velocity
75
+ + 2 * acceleration_time
76
+ + DEFAULT_TIMEOUT
77
+ )
78
+ move_status = self.user_setpoint.set(new_position, wait=True, timeout=timeout)
79
+ async for current_position in observe_value(
80
+ self.user_readback, done_status=move_status
81
+ ):
82
+ yield WatcherUpdate(
83
+ current=current_position,
84
+ initial=old_position,
85
+ target=new_position,
86
+ name=self.name,
87
+ unit=units,
88
+ precision=precision,
89
+ )
64
90
  if not self._set_success:
65
91
  raise RuntimeError("Motor was stopped")
66
92
 
67
- def move(self, new_position: float, timeout: Optional[float] = None):
68
- """Commandline only synchronous move of a Motor"""
69
- from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
70
-
71
- if in_bluesky_event_loop():
72
- raise RuntimeError("Will deadlock run engine if run in a plan")
73
- call_in_bluesky_event_loop(self._move(new_position), timeout) # type: ignore
74
-
75
- def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus:
76
- watchers: List[Callable] = []
77
- coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout)
78
- return AsyncStatus(coro, watchers)
79
-
80
93
  async def stop(self, success=False):
81
94
  self._set_success = success
82
95
  # Put with completion will never complete as we are waiting for completion on
83
96
  # the move above, so need to pass wait=False
84
- await self.stop_.execute(wait=False)
97
+ await self.motor_stop.trigger(wait=False)
98
+ # Trigger any callbacks
99
+ await self.user_readback._backend.put(await self.user_readback.get_value())
@@ -0,0 +1,3 @@
1
+ from .pvi import PVIEntry, create_children_from_annotations, fill_pvi_entries
2
+
3
+ __all__ = ["PVIEntry", "fill_pvi_entries", "create_children_from_annotations"]