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,286 @@
1
+ from enum import Enum
2
+ from functools import partial
3
+ from typing import Any, Callable, Dict, Generator, List, Optional, Sequence, Union
4
+
5
+ import numpy as np
6
+ import numpy.typing as npt
7
+ import yaml
8
+ from bluesky.plan_stubs import abs_set, wait
9
+ from bluesky.protocols import Location
10
+ from bluesky.utils import Msg
11
+ from epicscorelibs.ca.dbr import ca_array, ca_float, ca_int, ca_str
12
+
13
+ from .device import Device
14
+ from .signal import SignalRW
15
+
16
+ CaType = Union[ca_float, ca_int, ca_str, ca_array]
17
+
18
+
19
+ def ndarray_representer(dumper: yaml.Dumper, array: npt.NDArray[Any]) -> yaml.Node:
20
+ return dumper.represent_sequence(
21
+ "tag:yaml.org,2002:seq", array.tolist(), flow_style=True
22
+ )
23
+
24
+
25
+ def ca_dbr_representer(dumper: yaml.Dumper, value: CaType) -> yaml.Node:
26
+ # if it's an array, just call ndarray_representer...
27
+ represent_array = partial(ndarray_representer, dumper)
28
+
29
+ representers: Dict[CaType, Callable[[CaType], yaml.Node]] = {
30
+ ca_float: dumper.represent_float,
31
+ ca_int: dumper.represent_int,
32
+ ca_str: dumper.represent_str,
33
+ ca_array: represent_array,
34
+ }
35
+ return representers[type(value)](value)
36
+
37
+
38
+ class OphydDumper(yaml.Dumper):
39
+ def represent_data(self, data: Any) -> Any:
40
+ if isinstance(data, Enum):
41
+ return self.represent_data(data.value)
42
+ return super(OphydDumper, self).represent_data(data)
43
+
44
+
45
+ def get_signal_values(
46
+ signals: Dict[str, SignalRW[Any]], ignore: Optional[List[str]] = None
47
+ ) -> Generator[Msg, Sequence[Location[Any]], Dict[str, Any]]:
48
+ """Get signal values in bulk.
49
+
50
+ Used as part of saving the signals of a device to a yaml file.
51
+
52
+ Parameters
53
+ ----------
54
+ signals : Dict[str, SignalRW]
55
+ Dictionary with pv names and matching SignalRW values. Often the direct result
56
+ of :func:`walk_rw_signals`.
57
+
58
+ ignore : Optional[List[str]]
59
+ Optional list of PVs that should be ignored.
60
+
61
+ Returns
62
+ -------
63
+ Dict[str, Any]
64
+ A dictionary containing pv names and their associated values. Ignored pvs are
65
+ set to None.
66
+
67
+ See Also
68
+ --------
69
+ :func:`ophyd_async.core.walk_rw_signals`
70
+ :func:`ophyd_async.core.save_to_yaml`
71
+ """
72
+
73
+ ignore = ignore or []
74
+ selected_signals = {
75
+ key: signal for key, signal in signals.items() if key not in ignore
76
+ }
77
+ selected_values = yield Msg("locate", *selected_signals.values())
78
+
79
+ # TODO: investigate wrong type hints
80
+ if isinstance(selected_values, dict):
81
+ selected_values = [selected_values] # type: ignore
82
+
83
+ assert selected_values is not None, "No signalRW's were able to be located"
84
+ named_values = {
85
+ key: value["setpoint"] for key, value in zip(selected_signals, selected_values)
86
+ }
87
+ # Ignored values place in with value None so we know which ones were ignored
88
+ named_values.update({key: None for key in ignore})
89
+ return named_values
90
+
91
+
92
+ def walk_rw_signals(
93
+ device: Device, path_prefix: Optional[str] = ""
94
+ ) -> Dict[str, SignalRW[Any]]:
95
+ """Retrieve all SignalRWs from a device.
96
+
97
+ Stores retrieved signals with their dotted attribute paths in a dictionary. Used as
98
+ part of saving and loading a device.
99
+
100
+ Parameters
101
+ ----------
102
+ device : Device
103
+ Ophyd device to retrieve read-write signals from.
104
+
105
+ path_prefix : str
106
+ For internal use, leave blank when calling the method.
107
+
108
+ Returns
109
+ -------
110
+ SignalRWs : dict
111
+ A dictionary matching the string attribute path of a SignalRW with the
112
+ signal itself.
113
+
114
+ See Also
115
+ --------
116
+ :func:`ophyd_async.core.get_signal_values`
117
+ :func:`ophyd_async.core.save_to_yaml`
118
+
119
+ """
120
+
121
+ if not path_prefix:
122
+ path_prefix = ""
123
+
124
+ signals: Dict[str, SignalRW[Any]] = {}
125
+ for attr_name, attr in device.children():
126
+ dot_path = f"{path_prefix}{attr_name}"
127
+ if type(attr) is SignalRW:
128
+ signals[dot_path] = attr
129
+ attr_signals = walk_rw_signals(attr, path_prefix=dot_path + ".")
130
+ signals.update(attr_signals)
131
+ return signals
132
+
133
+
134
+ def save_to_yaml(phases: Sequence[Dict[str, Any]], save_path: str) -> None:
135
+ """Plan which serialises a phase or set of phases of SignalRWs to a yaml file.
136
+
137
+ Parameters
138
+ ----------
139
+ phases : dict or list of dicts
140
+ The values to save. Each item in the list is a seperate phase used when loading
141
+ a device. In general this variable be the return value of `get_signal_values`.
142
+
143
+ save_path : str
144
+ Path of the yaml file to write to
145
+
146
+ See Also
147
+ --------
148
+ :func:`ophyd_async.core.walk_rw_signals`
149
+ :func:`ophyd_async.core.get_signal_values`
150
+ :func:`ophyd_async.core.load_from_yaml`
151
+ """
152
+
153
+ yaml.add_representer(np.ndarray, ndarray_representer, Dumper=yaml.Dumper)
154
+
155
+ yaml.add_representer(ca_float, ca_dbr_representer, Dumper=yaml.Dumper)
156
+ yaml.add_representer(ca_int, ca_dbr_representer, Dumper=yaml.Dumper)
157
+ yaml.add_representer(ca_str, ca_dbr_representer, Dumper=yaml.Dumper)
158
+ yaml.add_representer(ca_array, ca_dbr_representer, Dumper=yaml.Dumper)
159
+
160
+ with open(save_path, "w") as file:
161
+ yaml.dump(phases, file, Dumper=OphydDumper, default_flow_style=False)
162
+
163
+
164
+ def load_from_yaml(save_path: str) -> Sequence[Dict[str, Any]]:
165
+ """Plan that returns a list of dicts with saved signal values from a yaml file.
166
+
167
+ Parameters
168
+ ----------
169
+ save_path : str
170
+ Path of the yaml file to load from
171
+
172
+ See Also
173
+ --------
174
+ :func:`ophyd_async.core.save_to_yaml`
175
+ :func:`ophyd_async.core.set_signal_values`
176
+ """
177
+ with open(save_path, "r") as file:
178
+ return yaml.full_load(file)
179
+
180
+
181
+ def set_signal_values(
182
+ signals: Dict[str, SignalRW[Any]], values: Sequence[Dict[str, Any]]
183
+ ) -> Generator[Msg, None, None]:
184
+ """Maps signals from a yaml file into device signals.
185
+
186
+ ``values`` contains signal values in phases, which are loaded in sequentially
187
+ into the provided signals, to ensure signals are set in the correct order.
188
+
189
+ Parameters
190
+ ----------
191
+ signals : Dict[str, SignalRW[Any]]
192
+ Dictionary of named signals to be updated if value found in values argument.
193
+ Can be the output of :func:`walk_rw_signals()` for a device.
194
+
195
+ values : Sequence[Dict[str, Any]]
196
+ List of dictionaries of signal name and value pairs, if a signal matches
197
+ the name of one in the signals argument, sets the signal to that value.
198
+ The groups of signals are loaded in their list order.
199
+ Can be the output of :func:`load_from_yaml()` for a yaml file.
200
+
201
+ See Also
202
+ --------
203
+ :func:`ophyd_async.core.load_from_yaml`
204
+ :func:`ophyd_async.core.walk_rw_signals`
205
+ """
206
+ # For each phase, set all the signals,
207
+ # load them to the correct value and wait for the load to complete
208
+ for phase_number, phase in enumerate(values):
209
+ # Key is signal name
210
+ for key, value in phase.items():
211
+ # Skip ignored values
212
+ if value is None:
213
+ continue
214
+
215
+ if key in signals:
216
+ yield from abs_set(
217
+ signals[key], value, group=f"load-phase{phase_number}"
218
+ )
219
+
220
+ yield from wait(f"load-phase{phase_number}")
221
+
222
+
223
+ def load_device(device: Device, path: str):
224
+ """Plan which loads PVs from a yaml file into a device.
225
+
226
+ Parameters
227
+ ----------
228
+ device: Device
229
+ The device to load PVs into
230
+ path: str
231
+ Path of the yaml file to load
232
+
233
+ See Also
234
+ --------
235
+ :func:`ophyd_async.core.save_device`
236
+ """
237
+ values = load_from_yaml(path)
238
+ signals_to_set = walk_rw_signals(device)
239
+ yield from set_signal_values(signals_to_set, values)
240
+
241
+
242
+ def all_at_once(values: Dict[str, Any]) -> Sequence[Dict[str, Any]]:
243
+ """Sort all the values into a single phase so they are set all at once"""
244
+ return [values]
245
+
246
+
247
+ def save_device(
248
+ device: Device,
249
+ path: str,
250
+ sorter: Callable[[Dict[str, Any]], Sequence[Dict[str, Any]]] = all_at_once,
251
+ ignore: Optional[List[str]] = None,
252
+ ):
253
+ """Plan that saves the state of all PV's on a device using a sorter.
254
+
255
+ The default sorter assumes all saved PVs can be loaded at once, and therefore
256
+ can be saved at one time, i.e. all PVs will appear on one list in the
257
+ resulting yaml file.
258
+
259
+ This can be a problem, because when the yaml is ingested with
260
+ :func:`ophyd_async.core.load_device`, it will set all of those PVs at once.
261
+ However, some PV's need to be set before others - this is device specific.
262
+
263
+ Therefore, users should consider the order of device loading and write their
264
+ own sorter algorithms accordingly.
265
+
266
+ See :func:`ophyd_async.panda.phase_sorter` for a valid implementation of the
267
+ sorter.
268
+
269
+ Parameters
270
+ ----------
271
+ device : Device
272
+ The device whose PVs should be saved.
273
+
274
+ path : str
275
+ The path where the resulting yaml should be saved to
276
+
277
+ sorter : Callable[[Dict[str, Any]], Sequence[Dict[str, Any]]]
278
+
279
+ ignore : Optional[List[str]]
280
+
281
+ See Also
282
+ --------
283
+ :func:`ophyd_async.core.load_device`
284
+ """
285
+ values = yield from get_signal_values(walk_rw_signals(device), ignore=ignore)
286
+ save_to_yaml(sorter(values), path)
@@ -0,0 +1,94 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, Generic, Optional, Sequence, TypeVar
3
+
4
+ from bluesky.protocols import Descriptor, Flyable, Preparable, Reading, Stageable
5
+
6
+ from .async_status import AsyncStatus
7
+ from .detector import TriggerInfo
8
+ from .device import Device
9
+ from .signal import SignalR
10
+ from .utils import merge_gathered_dicts
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ class TriggerLogic(ABC, Generic[T]):
16
+ @abstractmethod
17
+ def trigger_info(self, value: T) -> TriggerInfo:
18
+ """Return info about triggers that will be produced for a given value"""
19
+
20
+ @abstractmethod
21
+ async def prepare(self, value: T):
22
+ """Move to the start of the flyscan"""
23
+
24
+ @abstractmethod
25
+ async def start(self):
26
+ """Start the flyscan"""
27
+
28
+ @abstractmethod
29
+ async def stop(self):
30
+ """Stop flying and wait everything to be stopped"""
31
+
32
+
33
+ class HardwareTriggeredFlyable(
34
+ Device,
35
+ Stageable,
36
+ Preparable,
37
+ Flyable,
38
+ Generic[T],
39
+ ):
40
+ def __init__(
41
+ self,
42
+ trigger_logic: TriggerLogic[T],
43
+ configuration_signals: Sequence[SignalR],
44
+ name: str = "",
45
+ ):
46
+ self._trigger_logic = trigger_logic
47
+ self._configuration_signals = tuple(configuration_signals)
48
+ self._describe: Dict[str, Descriptor] = {}
49
+ self._fly_status: Optional[AsyncStatus] = None
50
+ self._trigger_info: Optional[TriggerInfo] = None
51
+ super().__init__(name=name)
52
+
53
+ @property
54
+ def trigger_logic(self) -> TriggerLogic[T]:
55
+ return self._trigger_logic
56
+
57
+ @property
58
+ def trigger_info(self) -> Optional[TriggerInfo]:
59
+ return self._trigger_info
60
+
61
+ @AsyncStatus.wrap
62
+ async def stage(self) -> None:
63
+ await self.unstage()
64
+
65
+ @AsyncStatus.wrap
66
+ async def unstage(self) -> None:
67
+ await self._trigger_logic.stop()
68
+
69
+ def prepare(self, value: T) -> AsyncStatus:
70
+ """Setup trajectories"""
71
+ return AsyncStatus(self._prepare(value))
72
+
73
+ async def _prepare(self, value: T) -> None:
74
+ self._trigger_info = self._trigger_logic.trigger_info(value)
75
+ # Move to start and setup the flyscan
76
+ await self._trigger_logic.prepare(value)
77
+
78
+ @AsyncStatus.wrap
79
+ async def kickoff(self) -> None:
80
+ self._fly_status = AsyncStatus(self._trigger_logic.start())
81
+
82
+ def complete(self) -> AsyncStatus:
83
+ assert self._fly_status, "Kickoff not run"
84
+ return self._fly_status
85
+
86
+ async def describe_configuration(self) -> Dict[str, Descriptor]:
87
+ return await merge_gathered_dicts(
88
+ [sig.describe() for sig in self._configuration_signals]
89
+ )
90
+
91
+ async def read_configuration(self) -> Dict[str, Reading]:
92
+ return await merge_gathered_dicts(
93
+ [sig.read() for sig in self._configuration_signals]
94
+ )
@@ -6,6 +6,8 @@ from typing import AsyncGenerator, Callable, Dict, Generic, Optional, Union
6
6
 
7
7
  from bluesky.protocols import (
8
8
  Descriptor,
9
+ Locatable,
10
+ Location,
9
11
  Movable,
10
12
  Readable,
11
13
  Reading,
@@ -13,11 +15,11 @@ from bluesky.protocols import (
13
15
  Subscribable,
14
16
  )
15
17
 
16
- from ...async_status import AsyncStatus
17
- from ...utils import DEFAULT_TIMEOUT, Callback, ReadingValueCallback, T
18
- from .._backend.signal_backend import SignalBackend
19
- from .._backend.sim_signal_backend import SimSignalBackend
20
- from ..device import Device
18
+ from .async_status import AsyncStatus
19
+ from .device import Device
20
+ from .signal_backend import SignalBackend
21
+ from .sim_signal_backend import SimSignalBackend
22
+ from .utils import DEFAULT_TIMEOUT, Callback, ReadingValueCallback, T
21
23
 
22
24
  _sim_backends: Dict[Signal, SimSignalBackend] = {}
23
25
 
@@ -56,7 +58,7 @@ class Signal(Device, Generic[T]):
56
58
  def set_name(self, name: str = ""):
57
59
  self._name = name
58
60
 
59
- async def connect(self, sim=False):
61
+ async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT):
60
62
  if sim:
61
63
  self._backend = SimSignalBackend(
62
64
  datatype=self._init_backend.datatype, source=self._init_backend.source
@@ -65,7 +67,7 @@ class Signal(Device, Generic[T]):
65
67
  else:
66
68
  self._backend = self._init_backend
67
69
  _sim_backends.pop(self, None)
68
- await self._backend.connect()
70
+ await self._backend.connect(timeout=timeout)
69
71
 
70
72
  @property
71
73
  def source(self) -> str:
@@ -196,25 +198,40 @@ class SignalR(Signal[T], Readable, Stageable, Subscribable):
196
198
  self._del_cache(self._get_cache().set_staged(False))
197
199
 
198
200
 
201
+ USE_DEFAULT_TIMEOUT = "USE_DEFAULT_TIMEOUT"
202
+
203
+
199
204
  class SignalW(Signal[T], Movable):
200
205
  """Signal that can be set"""
201
206
 
202
- def set(self, value: T, wait=True, timeout=None) -> AsyncStatus:
207
+ def set(self, value: T, wait=True, timeout=USE_DEFAULT_TIMEOUT) -> AsyncStatus:
203
208
  """Set the value and return a status saying when it's done"""
204
- coro = self._backend.put(value, wait=wait, timeout=timeout or self._timeout)
209
+ if timeout is USE_DEFAULT_TIMEOUT:
210
+ timeout = self._timeout
211
+ coro = self._backend.put(value, wait=wait, timeout=timeout)
205
212
  return AsyncStatus(coro)
206
213
 
207
214
 
208
- class SignalRW(SignalR[T], SignalW[T]):
215
+ class SignalRW(SignalR[T], SignalW[T], Locatable):
209
216
  """Signal that can be both read and set"""
210
217
 
218
+ async def locate(self) -> Location:
219
+ location: Location = {
220
+ "setpoint": await self._backend.get_setpoint(),
221
+ "readback": await self.get_value(),
222
+ }
223
+ return location
224
+
211
225
 
212
226
  class SignalX(Signal):
213
227
  """Signal that puts the default value"""
214
228
 
215
- async def execute(self, wait=True, timeout=None):
216
- """Execute the action and return a status saying when it's done"""
217
- await self._backend.put(None, wait=wait, timeout=timeout or self._timeout)
229
+ def trigger(self, wait=True, timeout=USE_DEFAULT_TIMEOUT) -> AsyncStatus:
230
+ """Trigger the action and return a status saying when it's done"""
231
+ if timeout is USE_DEFAULT_TIMEOUT:
232
+ timeout = self._timeout
233
+ coro = self._backend.put(None, wait=wait, timeout=timeout)
234
+ return AsyncStatus(coro)
218
235
 
219
236
 
220
237
  def set_sim_value(signal: Signal[T], value: T):
@@ -236,7 +253,7 @@ def set_sim_callback(signal: Signal[T], callback: ReadingValueCallback[T]) -> No
236
253
  return _sim_backends[signal].set_callback(callback)
237
254
 
238
255
 
239
- async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
256
+ async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, None]:
240
257
  """Subscribe to the value of a signal so it can be iterated from.
241
258
 
242
259
  Parameters
@@ -253,17 +270,24 @@ async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
253
270
  do_something_with(value)
254
271
  """
255
272
  q: asyncio.Queue[T] = asyncio.Queue()
273
+ if timeout is None:
274
+ get_value = q.get
275
+ else:
276
+
277
+ async def get_value():
278
+ return await asyncio.wait_for(q.get(), timeout)
279
+
256
280
  signal.subscribe_value(q.put_nowait)
257
281
  try:
258
282
  while True:
259
- yield await q.get()
283
+ yield await get_value()
260
284
  finally:
261
285
  signal.clear_sub(q.put_nowait)
262
286
 
263
287
 
264
288
  class _ValueChecker(Generic[T]):
265
289
  def __init__(self, matcher: Callable[[T], bool], matcher_name: str):
266
- self._last_value: Optional[T]
290
+ self._last_value: Optional[T] = None
267
291
  self._matcher = matcher
268
292
  self._matcher_name = matcher_name
269
293
 
@@ -273,7 +297,7 @@ class _ValueChecker(Generic[T]):
273
297
  if self._matcher(value):
274
298
  return
275
299
 
276
- async def wait_for_value(self, signal: SignalR[T], timeout: float):
300
+ async def wait_for_value(self, signal: SignalR[T], timeout: Optional[float]):
277
301
  try:
278
302
  await asyncio.wait_for(self._wait_for_value(signal), timeout)
279
303
  except asyncio.TimeoutError as e:
@@ -284,7 +308,7 @@ class _ValueChecker(Generic[T]):
284
308
 
285
309
 
286
310
  async def wait_for_value(
287
- signal: SignalR[T], match: Union[T, Callable[[T], bool]], timeout: float
311
+ signal: SignalR[T], match: Union[T, Callable[[T], bool]], timeout: Optional[float]
288
312
  ):
289
313
  """Wait for a signal to have a matching value.
290
314
 
@@ -330,6 +354,10 @@ async def set_and_wait_for_value(
330
354
  - Read the same Signal to check the operation has started
331
355
  - Return the Status so calling code can wait for operation to complete
332
356
 
357
+ This function sets a signal to a specified value, optionally with or without a
358
+ ca/pv put callback, and waits for the readback value of the signal to match the
359
+ value it was set to.
360
+
333
361
  Parameters
334
362
  ----------
335
363
  signal:
@@ -3,7 +3,7 @@ from typing import Generic, Optional, Type
3
3
 
4
4
  from bluesky.protocols import Descriptor, Reading
5
5
 
6
- from ...utils import ReadingValueCallback, T
6
+ from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T
7
7
 
8
8
 
9
9
  class SignalBackend(Generic[T]):
@@ -16,7 +16,7 @@ class SignalBackend(Generic[T]):
16
16
  source: str = ""
17
17
 
18
18
  @abstractmethod
19
- async def connect(self):
19
+ async def connect(self, timeout: float = DEFAULT_TIMEOUT):
20
20
  """Connect to underlying hardware"""
21
21
 
22
22
  @abstractmethod
@@ -35,6 +35,10 @@ class SignalBackend(Generic[T]):
35
35
  async def get_value(self) -> T:
36
36
  """The current value"""
37
37
 
38
+ @abstractmethod
39
+ async def get_setpoint(self) -> T:
40
+ """The point that a signal was requested to move to."""
41
+
38
42
  @abstractmethod
39
43
  def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
40
44
  """Observe changes to the current value, timestamp and severity"""
@@ -11,8 +11,8 @@ from typing import Any, Dict, Generic, Optional, Type, Union, cast, get_origin
11
11
 
12
12
  from bluesky.protocols import Descriptor, Dtype, Reading
13
13
 
14
- from ...utils import ReadingValueCallback, T, get_dtype
15
14
  from .signal_backend import SignalBackend
15
+ from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T, get_dtype
16
16
 
17
17
  primitive_dtypes: Dict[type, Dtype] = {
18
18
  str: "string",
@@ -123,7 +123,7 @@ class SimSignalBackend(SignalBackend[T]):
123
123
  self.put_proceeds.set()
124
124
  self.callback: Optional[ReadingValueCallback[T]] = None
125
125
 
126
- async def connect(self) -> None:
126
+ async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
127
127
  self.converter = make_converter(self.datatype)
128
128
  self._initial_value = self.converter.make_initial_value(self.datatype)
129
129
  self._severity = 0
@@ -161,6 +161,10 @@ class SimSignalBackend(SignalBackend[T]):
161
161
  async def get_value(self) -> T:
162
162
  return self.converter.value(self._value)
163
163
 
164
+ async def get_setpoint(self) -> T:
165
+ """For a simulated backend, the setpoint and readback values are the same."""
166
+ return await self.get_value()
167
+
164
168
  def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
165
169
  if callback:
166
170
  assert not self.callback, "Cannot set a callback when one is already set"
@@ -2,10 +2,10 @@ from typing import Dict, Sequence, Tuple
2
2
 
3
3
  from bluesky.protocols import Configurable, Descriptor, Readable, Reading, Stageable
4
4
 
5
- from ..async_status import AsyncStatus
6
- from ..utils import merge_gathered_dicts
7
- from ._signal.signal import SignalR
5
+ from .async_status import AsyncStatus
8
6
  from .device import Device
7
+ from .signal import SignalR
8
+ from .utils import merge_gathered_dicts
9
9
 
10
10
 
11
11
  class StandardReadable(Device, Readable, Configurable, Stageable):