ophyd-async 0.2.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 (79) hide show
  1. ophyd_async/__init__.py +1 -4
  2. ophyd_async/_version.py +2 -2
  3. ophyd_async/core/__init__.py +52 -19
  4. ophyd_async/core/_providers.py +38 -5
  5. ophyd_async/core/async_status.py +86 -40
  6. ophyd_async/core/detector.py +214 -72
  7. ophyd_async/core/device.py +91 -50
  8. ophyd_async/core/device_save_loader.py +96 -23
  9. ophyd_async/core/flyer.py +32 -246
  10. ophyd_async/core/mock_signal_backend.py +82 -0
  11. ophyd_async/core/mock_signal_utils.py +145 -0
  12. ophyd_async/core/signal.py +225 -58
  13. ophyd_async/core/signal_backend.py +8 -5
  14. ophyd_async/core/{sim_signal_backend.py → soft_signal_backend.py} +51 -49
  15. ophyd_async/core/standard_readable.py +212 -23
  16. ophyd_async/core/utils.py +123 -30
  17. ophyd_async/epics/_backend/_aioca.py +42 -44
  18. ophyd_async/epics/_backend/_p4p.py +96 -52
  19. ophyd_async/epics/_backend/common.py +25 -0
  20. ophyd_async/epics/areadetector/__init__.py +8 -4
  21. ophyd_async/epics/areadetector/aravis.py +63 -0
  22. ophyd_async/epics/areadetector/controllers/__init__.py +2 -1
  23. ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +1 -1
  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 +37 -25
  27. ophyd_async/epics/areadetector/controllers/vimba_controller.py +66 -0
  28. ophyd_async/epics/areadetector/drivers/__init__.py +6 -0
  29. ophyd_async/epics/areadetector/drivers/ad_base.py +8 -12
  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 +8 -5
  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 +14 -6
  37. ophyd_async/epics/areadetector/utils.py +2 -12
  38. ophyd_async/epics/areadetector/vimba.py +43 -0
  39. ophyd_async/epics/areadetector/writers/_hdffile.py +21 -7
  40. ophyd_async/epics/areadetector/writers/hdf_writer.py +32 -17
  41. ophyd_async/epics/areadetector/writers/nd_file_hdf.py +19 -18
  42. ophyd_async/epics/areadetector/writers/nd_plugin.py +15 -7
  43. ophyd_async/epics/demo/__init__.py +75 -49
  44. ophyd_async/epics/motion/motor.py +67 -53
  45. ophyd_async/epics/pvi/__init__.py +3 -0
  46. ophyd_async/epics/pvi/pvi.py +318 -0
  47. ophyd_async/epics/signal/__init__.py +8 -3
  48. ophyd_async/epics/signal/signal.py +26 -9
  49. ophyd_async/log.py +130 -0
  50. ophyd_async/panda/__init__.py +21 -5
  51. ophyd_async/panda/_common_blocks.py +49 -0
  52. ophyd_async/panda/_hdf_panda.py +48 -0
  53. ophyd_async/panda/_panda_controller.py +37 -0
  54. ophyd_async/panda/_trigger.py +39 -0
  55. ophyd_async/panda/_utils.py +15 -0
  56. ophyd_async/panda/writers/__init__.py +3 -0
  57. ophyd_async/panda/writers/_hdf_writer.py +220 -0
  58. ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
  59. ophyd_async/plan_stubs/__init__.py +13 -0
  60. ophyd_async/plan_stubs/ensure_connected.py +22 -0
  61. ophyd_async/plan_stubs/fly.py +149 -0
  62. ophyd_async/protocols.py +126 -0
  63. ophyd_async/sim/__init__.py +11 -0
  64. ophyd_async/sim/demo/__init__.py +3 -0
  65. ophyd_async/sim/demo/sim_motor.py +103 -0
  66. ophyd_async/sim/pattern_generator.py +318 -0
  67. ophyd_async/sim/sim_pattern_detector_control.py +55 -0
  68. ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
  69. ophyd_async/sim/sim_pattern_generator.py +37 -0
  70. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3.0.dist-info}/METADATA +31 -70
  71. ophyd_async-0.3.0.dist-info/RECORD +86 -0
  72. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3.0.dist-info}/WHEEL +1 -1
  73. ophyd_async/epics/signal/pvi_get.py +0 -22
  74. ophyd_async/panda/panda.py +0 -294
  75. ophyd_async-0.2.0.dist-info/RECORD +0 -53
  76. /ophyd_async/panda/{table.py → _table.py} +0 -0
  77. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3.0.dist-info}/LICENSE +0 -0
  78. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3.0.dist-info}/entry_points.txt +0 -0
  79. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3.0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,8 @@
1
1
  import asyncio
2
- from typing import AsyncIterator, Dict, List, Optional
2
+ from pathlib import Path
3
+ from typing import AsyncGenerator, AsyncIterator, Dict, List, Optional
3
4
 
4
- from bluesky.protocols import Asset, Descriptor, Hints
5
+ from bluesky.protocols import DataKey, Hints, StreamAsset
5
6
 
6
7
  from ophyd_async.core import (
7
8
  DEFAULT_TIMEOUT,
@@ -13,6 +14,7 @@ from ophyd_async.core import (
13
14
  set_and_wait_for_value,
14
15
  wait_for_value,
15
16
  )
17
+ from ophyd_async.core.signal import observe_value
16
18
 
17
19
  from ._hdfdataset import _HDFDataset
18
20
  from ._hdffile import _HDFFile
@@ -38,22 +40,27 @@ class HDFWriter(DetectorWriter):
38
40
  self._file: Optional[_HDFFile] = None
39
41
  self._multiplier = 1
40
42
 
41
- async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]:
43
+ async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
42
44
  self._file = None
43
45
  info = self._directory_provider()
46
+ file_path = str(info.root / info.resource_dir)
44
47
  await asyncio.gather(
45
48
  self.hdf.num_extra_dims.set(0),
46
49
  self.hdf.lazy_open.set(True),
47
50
  self.hdf.swmr_mode.set(True),
48
- self.hdf.file_path.set(info.directory_path),
49
- self.hdf.file_name.set(f"{info.filename_prefix}{self.hdf.name}"),
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}"),
50
54
  self.hdf.file_template.set("%s/%s.h5"),
51
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(""),
52
59
  )
53
60
 
54
61
  assert (
55
62
  await self.hdf.file_path_exists.get_value()
56
- ), f"File path {info.directory_path} for hdf plugin does not exist"
63
+ ), f"File path {file_path} for hdf plugin does not exist"
57
64
 
58
65
  # Overwrite num_capture to go forever
59
66
  await self.hdf.num_capture.set(0)
@@ -78,7 +85,7 @@ class HDFWriter(DetectorWriter):
78
85
  )
79
86
  )
80
87
  describe = {
81
- ds.name: Descriptor(
88
+ ds.name: DataKey(
82
89
  source=self.hdf.full_file_name.source,
83
90
  shape=outer_shape + tuple(ds.shape),
84
91
  dtype="array" if ds.shape else "number",
@@ -88,27 +95,35 @@ class HDFWriter(DetectorWriter):
88
95
  }
89
96
  return describe
90
97
 
91
- async def wait_for_index(
92
- self, index: int, timeout: Optional[float] = DEFAULT_TIMEOUT
93
- ):
94
- def matcher(value: int) -> bool:
95
- return value // self._multiplier >= index
96
-
97
- matcher.__name__ = f"index_at_least_{index}"
98
- await wait_for_value(self.hdf.num_captured, matcher, timeout=timeout)
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
99
104
 
100
105
  async def get_indices_written(self) -> int:
101
106
  num_captured = await self.hdf.num_captured.get_value()
102
107
  return num_captured // self._multiplier
103
108
 
104
- async def collect_stream_docs(self, indices_written: int) -> AsyncIterator[Asset]:
109
+ async def collect_stream_docs(
110
+ self, indices_written: int
111
+ ) -> AsyncIterator[StreamAsset]:
105
112
  # TODO: fail if we get dropped frames
106
113
  await self.hdf.flush_now.set(True)
107
114
  if indices_written:
108
115
  if not self._file:
116
+ path = Path(await self.hdf.full_file_name.get_value())
109
117
  self._file = _HDFFile(
110
- await self.hdf.full_file_name.get_value(), self._datasets
118
+ self._directory_provider(),
119
+ # See https://github.com/bluesky/ophyd-async/issues/122
120
+ path,
121
+ self._datasets,
111
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
+
112
127
  for doc in self._file.stream_resources():
113
128
  yield "stream_resource", doc
114
129
  for doc in self._file.stream_data(indices_written):
@@ -1,7 +1,7 @@
1
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_r, epics_signal_rw, epics_signal_rw_rbv
4
+ from ..utils import FileWriteMode
5
5
  from .nd_plugin import NDPluginBase
6
6
 
7
7
 
@@ -19,21 +19,22 @@ class Compression(str, Enum):
19
19
  class NDFileHDF(NDPluginBase):
20
20
  def __init__(self, prefix: str, name="") -> None:
21
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")
25
- self.file_path = ad_rw(str, prefix + "FilePath")
26
- self.file_name = ad_rw(str, prefix + "FileName")
27
- self.file_path_exists = ad_r(bool, prefix + "FilePathExists")
28
- self.file_template = ad_rw(str, prefix + "FileTemplate")
29
- self.full_file_name = ad_r(str, prefix + "FullFileName")
30
- self.file_write_mode = ad_rw(FileWriteMode, prefix + "FileWriteMode")
31
- self.num_capture = ad_rw(int, prefix + "NumCapture")
32
- self.num_captured = ad_r(int, prefix + "NumCaptured")
33
- self.swmr_mode = ad_rw(bool, prefix + "SWMRMode")
34
- self.lazy_open = ad_rw(bool, prefix + "LazyOpen")
35
- self.capture = ad_rw(bool, prefix + "Capture")
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")
36
38
  self.flush_now = epics_signal_rw(bool, prefix + "FlushNow")
37
- self.array_size0 = ad_r(int, prefix + "ArraySize0")
38
- self.array_size1 = ad_r(int, prefix + "ArraySize1")
39
+ self.xml_file_name = epics_signal_rw_rbv(str, prefix + "XMLFileName")
39
40
  super().__init__(prefix, name)
@@ -2,8 +2,7 @@ from enum import Enum
2
2
 
3
3
  from ophyd_async.core import Device
4
4
  from ophyd_async.epics.signal import epics_signal_rw
5
-
6
- from ..utils import ad_r, ad_rw
5
+ from ophyd_async.epics.signal.signal import epics_signal_r, epics_signal_rw_rbv
7
6
 
8
7
 
9
8
  class Callback(str, Enum):
@@ -13,16 +12,25 @@ class Callback(str, Enum):
13
12
 
14
13
  class NDArrayBase(Device):
15
14
  def __init__(self, prefix: str, name: str = "") -> None:
16
- self.unique_id = ad_r(int, prefix + "UniqueId")
15
+ self.unique_id = epics_signal_r(int, prefix + "UniqueId_RBV")
17
16
  self.nd_attributes_file = epics_signal_rw(str, prefix + "NDAttributesFile")
18
- super().__init__(name)
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)
19
25
 
20
26
 
21
27
  class NDPluginBase(NDArrayBase):
22
28
  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")
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")
26
34
  super().__init__(prefix, name)
27
35
 
28
36
 
@@ -6,15 +6,28 @@ 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
 
@@ -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,47 +88,43 @@ 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
130
  status = self.stop_.trigger()
@@ -135,11 +150,22 @@ def start_ioc_subprocess() -> str:
135
150
  pv_prefix = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) + ":"
136
151
  here = Path(__file__).absolute().parent
137
152
  args = [sys.executable, "-m", "epicscorelibs.ioc"]
153
+
154
+ # Create standalone sensor
138
155
  args += ["-m", f"P={pv_prefix}"]
139
156
  args += ["-d", str(here / "sensor.db")]
140
- for suff in "XY":
141
- 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}:"]
142
166
  args += ["-d", str(here / "mover.db")]
167
+
168
+ # Start IOC
143
169
  process = subprocess.Popen(
144
170
  args,
145
171
  stdin=subprocess.PIPE,
@@ -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,72 +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
- status = self.stop_.trigger(wait=False)
85
- await status
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"]