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
@@ -45,30 +45,29 @@ class OphydDumper(yaml.Dumper):
45
45
  def get_signal_values(
46
46
  signals: Dict[str, SignalRW[Any]], ignore: Optional[List[str]] = None
47
47
  ) -> Generator[Msg, Sequence[Location[Any]], Dict[str, Any]]:
48
- """
49
- Read the values of SignalRW's, to be used alongside `walk_rw_signals`. Used as part
50
- of saving a device.
48
+ """Get signal values in bulk.
49
+
50
+ Used as part of saving the signals of a device to a yaml file.
51
+
51
52
  Parameters
52
53
  ----------
53
- signals : Dict[str, SignalRW]: A dictionary matching the string attribute path
54
- of a SignalRW to the signal itself
54
+ signals : Dict[str, SignalRW]
55
+ Dictionary with pv names and matching SignalRW values. Often the direct result
56
+ of :func:`walk_rw_signals`.
55
57
 
56
- ignore : List of strings: . A list of string attribute paths to the SignalRW's
57
- to be ignored.
58
+ ignore : Optional[List[str]]
59
+ Optional list of PVs that should be ignored.
58
60
 
59
61
  Returns
60
62
  -------
61
- Dict[str, Any]: A dictionary matching the string attribute path of a SignalRW
62
- to the value of that signal. Ignored attributes are set to None.
63
-
64
- Yields:
65
- Iterator[Dict[str, Any]]: The Location of a signal
63
+ Dict[str, Any]
64
+ A dictionary containing pv names and their associated values. Ignored pvs are
65
+ set to None.
66
66
 
67
67
  See Also
68
68
  --------
69
69
  :func:`ophyd_async.core.walk_rw_signals`
70
70
  :func:`ophyd_async.core.save_to_yaml`
71
-
72
71
  """
73
72
 
74
73
  ignore = ignore or []
@@ -93,9 +92,11 @@ def get_signal_values(
93
92
  def walk_rw_signals(
94
93
  device: Device, path_prefix: Optional[str] = ""
95
94
  ) -> Dict[str, SignalRW[Any]]:
96
- """
97
- Get all the SignalRWs from a device and store them with their dotted attribute
98
- paths in a dictionary. Used as part of saving and loading a device
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
+
99
100
  Parameters
100
101
  ----------
101
102
  device : Device
@@ -131,9 +132,7 @@ def walk_rw_signals(
131
132
 
132
133
 
133
134
  def save_to_yaml(phases: Sequence[Dict[str, Any]], save_path: str) -> None:
134
- """
135
- Serialises and saves a phase or a set of phases of a device's SignalRW's to a
136
- yaml file.
135
+ """Plan which serialises a phase or set of phases of SignalRWs to a yaml file.
137
136
 
138
137
  Parameters
139
138
  ----------
@@ -163,8 +162,7 @@ def save_to_yaml(phases: Sequence[Dict[str, Any]], save_path: str) -> None:
163
162
 
164
163
 
165
164
  def load_from_yaml(save_path: str) -> Sequence[Dict[str, Any]]:
166
- """
167
- Returns a list of dicts with saved signal values from a yaml file
165
+ """Plan that returns a list of dicts with saved signal values from a yaml file.
168
166
 
169
167
  Parameters
170
168
  ----------
@@ -183,18 +181,22 @@ def load_from_yaml(save_path: str) -> Sequence[Dict[str, Any]]:
183
181
  def set_signal_values(
184
182
  signals: Dict[str, SignalRW[Any]], values: Sequence[Dict[str, Any]]
185
183
  ) -> Generator[Msg, None, None]:
186
- """
187
- Loads a phase or a set of phases from a yaml file and puts value to an Ophyd device
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
188
 
189
189
  Parameters
190
190
  ----------
191
191
  signals : Dict[str, SignalRW[Any]]
192
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.
193
194
 
194
195
  values : Sequence[Dict[str, Any]]
195
196
  List of dictionaries of signal name and value pairs, if a signal matches
196
197
  the name of one in the signals argument, sets the signal to that value.
197
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.
198
200
 
199
201
  See Also
200
202
  --------
@@ -206,8 +208,79 @@ def set_signal_values(
206
208
  for phase_number, phase in enumerate(values):
207
209
  # Key is signal name
208
210
  for key, value in phase.items():
211
+ # Skip ignored values
212
+ if value is None:
213
+ continue
214
+
209
215
  if key in signals:
210
216
  yield from abs_set(
211
217
  signals[key], value, group=f"load-phase{phase_number}"
212
218
  )
219
+
213
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)
ophyd_async/core/flyer.py CHANGED
@@ -1,170 +1,29 @@
1
- import asyncio
2
- import time
3
1
  from abc import ABC, abstractmethod
4
- from dataclasses import dataclass
5
- from typing import (
6
- AsyncIterator,
7
- Callable,
8
- Dict,
9
- Generic,
10
- List,
11
- Optional,
12
- Sequence,
13
- TypeVar,
14
- )
2
+ from typing import Dict, Generic, Sequence, TypeVar
15
3
 
16
- from bluesky.protocols import (
17
- Asset,
18
- Collectable,
19
- Descriptor,
20
- Flyable,
21
- HasHints,
22
- Hints,
23
- Movable,
24
- Reading,
25
- Stageable,
26
- WritesExternalAssets,
27
- )
4
+ from bluesky.protocols import DataKey, Flyable, Preparable, Reading, Stageable
28
5
 
29
6
  from .async_status import AsyncStatus
30
- from .detector import DetectorControl, DetectorTrigger, DetectorWriter
31
7
  from .device import Device
32
8
  from .signal import SignalR
33
- from .utils import DEFAULT_TIMEOUT, gather_list, merge_gathered_dicts
9
+ from .utils import merge_gathered_dicts
34
10
 
35
11
  T = TypeVar("T")
36
12
 
37
13
 
38
- @dataclass(frozen=True)
39
- class TriggerInfo:
40
- #: Number of triggers that will be sent
41
- num: int
42
- #: Sort of triggers that will be sent
43
- trigger: DetectorTrigger
44
- #: What is the minimum deadtime between triggers
45
- deadtime: float
46
- #: What is the maximum high time of the triggers
47
- livetime: float
48
-
49
-
50
- class DetectorGroupLogic(ABC):
51
- # Read multipliers here, exposure is set in the plan
52
-
53
- @abstractmethod
54
- async def open(self) -> Dict[str, Descriptor]:
55
- """Open all writers, wait for them to be open and return their descriptors"""
56
-
57
- @abstractmethod
58
- async def ensure_armed(self, trigger_info: TriggerInfo):
59
- """Ensure the detectors are armed, return AsyncStatus that waits for disarm."""
60
-
61
- @abstractmethod
62
- def collect_asset_docs(self) -> AsyncIterator[Asset]:
63
- """Collect asset docs from all writers"""
64
-
65
- @abstractmethod
66
- async def wait_for_index(
67
- self, index: int, timeout: Optional[float] = DEFAULT_TIMEOUT
68
- ):
69
- """Wait until a specific index is ready to be collected"""
70
-
71
- @abstractmethod
72
- async def disarm(self):
73
- """Disarm detectors"""
74
-
75
- @abstractmethod
76
- async def close(self):
77
- """Close all writers and wait for them to be closed"""
78
-
79
- @abstractmethod
80
- def hints(self) -> Hints:
81
- """Produce hints specifying which dataset(s) are most important"""
82
-
83
-
84
- class SameTriggerDetectorGroupLogic(DetectorGroupLogic):
85
- def __init__(
86
- self,
87
- controllers: Sequence[DetectorControl],
88
- writers: Sequence[DetectorWriter],
89
- ) -> None:
90
- self._controllers = controllers
91
- self._writers = writers
92
- self._arm_statuses: Sequence[AsyncStatus] = ()
93
- self._trigger_info: Optional[TriggerInfo] = None
94
-
95
- async def open(self) -> Dict[str, Descriptor]:
96
- return await merge_gathered_dicts(writer.open() for writer in self._writers)
97
-
98
- async def ensure_armed(self, trigger_info: TriggerInfo):
99
- if (
100
- not self._arm_statuses
101
- or any(status.done for status in self._arm_statuses)
102
- or trigger_info != self._trigger_info
103
- ):
104
- # We need to re-arm
105
- await self.disarm()
106
- for controller in self._controllers:
107
- required = controller.get_deadtime(trigger_info.livetime)
108
- assert required <= trigger_info.deadtime, (
109
- f"Detector {controller} needs at least {required}s deadtime, "
110
- f"but trigger logic provides only {trigger_info.deadtime}s"
111
- )
112
- self._arm_statuses = await gather_list(
113
- controller.arm(
114
- trigger=trigger_info.trigger, exposure=trigger_info.livetime
115
- )
116
- for controller in self._controllers
117
- )
118
- self._trigger_info = trigger_info
119
-
120
- async def collect_asset_docs(self) -> AsyncIterator[Asset]:
121
- # the below is confusing: gather_list does return an awaitable, but it itself
122
- # is a coroutine so we must call await twice...
123
- indices_written = min(
124
- await gather_list(writer.get_indices_written() for writer in self._writers)
125
- )
126
- for writer in self._writers:
127
- async for doc in writer.collect_stream_docs(indices_written):
128
- yield doc
129
-
130
- async def wait_for_index(
131
- self, index: int, timeout: Optional[float] = DEFAULT_TIMEOUT
132
- ):
133
- await gather_list(
134
- writer.wait_for_index(index, timeout=timeout) for writer in self._writers
135
- )
136
-
137
- async def disarm(self):
138
- await gather_list(controller.disarm() for controller in self._controllers)
139
- await gather_list(self._arm_statuses)
140
-
141
- async def close(self):
142
- await gather_list(writer.close() for writer in self._writers)
143
-
144
- def hints(self) -> Hints:
145
- return {
146
- "fields": [
147
- field
148
- for writer in self._writers
149
- if hasattr(writer, "hints")
150
- for field in writer.hints.get("fields")
151
- ]
152
- }
153
-
154
-
155
14
  class TriggerLogic(ABC, Generic[T]):
156
- @abstractmethod
157
- def trigger_info(self, value: T) -> TriggerInfo:
158
- """Return info about triggers that will be produced for a given value"""
159
-
160
15
  @abstractmethod
161
16
  async def prepare(self, value: T):
162
17
  """Move to the start of the flyscan"""
163
18
 
164
19
  @abstractmethod
165
- async def start(self):
20
+ async def kickoff(self):
166
21
  """Start the flyscan"""
167
22
 
23
+ @abstractmethod
24
+ async def complete(self):
25
+ """Block until the flyscan is done"""
26
+
168
27
  @abstractmethod
169
28
  async def stop(self):
170
29
  """Stop flying and wait everything to be stopped"""
@@ -172,63 +31,50 @@ class TriggerLogic(ABC, Generic[T]):
172
31
 
173
32
  class HardwareTriggeredFlyable(
174
33
  Device,
175
- Movable,
176
34
  Stageable,
35
+ Preparable,
177
36
  Flyable,
178
- Collectable,
179
- WritesExternalAssets,
180
- HasHints,
181
37
  Generic[T],
182
38
  ):
183
39
  def __init__(
184
40
  self,
185
- detector_group_logic: DetectorGroupLogic,
186
41
  trigger_logic: TriggerLogic[T],
187
- configuration_signals: Sequence[SignalR],
188
- trigger_to_frame_timeout: Optional[float] = DEFAULT_TIMEOUT,
42
+ configuration_signals: Sequence[SignalR] = (),
189
43
  name: str = "",
190
44
  ):
191
- self._detector_group_logic = detector_group_logic
192
45
  self._trigger_logic = trigger_logic
193
46
  self._configuration_signals = tuple(configuration_signals)
194
- self._describe: Dict[str, Descriptor] = {}
195
- self._watchers: List[Callable] = []
196
- self._fly_status: Optional[AsyncStatus] = None
197
- self._fly_start = 0.0
198
- self._offset = 0 # Add this to index to get frame number
199
- self._current_frame = 0 # The current frame we are on
200
- self._last_frame = 0 # The last frame that will be emitted
201
- self._trigger_to_frame_timeout = trigger_to_frame_timeout
202
47
  super().__init__(name=name)
203
48
 
49
+ @property
50
+ def trigger_logic(self) -> TriggerLogic[T]:
51
+ return self._trigger_logic
52
+
204
53
  @AsyncStatus.wrap
205
54
  async def stage(self) -> None:
206
55
  await self.unstage()
207
- self._describe = await self._detector_group_logic.open()
208
- self._offset = 0
209
- self._current_frame = 0
210
56
 
211
- def set(self, value: T) -> AsyncStatus:
212
- """Arm detectors and setup trajectories"""
213
- # index + offset = current_frame, but starting a new scan so want it to be 0
214
- # so subtract current_frame from both sides
215
- return AsyncStatus(self._set(value))
57
+ @AsyncStatus.wrap
58
+ async def unstage(self) -> None:
59
+ await self._trigger_logic.stop()
216
60
 
217
- async def _set(self, value: T) -> None:
218
- self._offset -= self._current_frame
219
- self._current_frame = 0
220
- await self._prepare(value)
61
+ def prepare(self, value: T) -> AsyncStatus:
62
+ """Setup trajectories"""
63
+ return AsyncStatus(self._prepare(value))
221
64
 
222
- async def _prepare(self, value: T):
223
- trigger_info = self._trigger_logic.trigger_info(value)
224
- # Move to start and setup the flyscan, and arm dets in parallel
225
- await asyncio.gather(
226
- self._detector_group_logic.ensure_armed(trigger_info),
227
- self._trigger_logic.prepare(value),
228
- )
229
- self._last_frame = self._current_frame + trigger_info.num
65
+ async def _prepare(self, value: T) -> None:
66
+ # Move to start and setup the flyscan
67
+ await self._trigger_logic.prepare(value)
230
68
 
231
- async def describe_configuration(self) -> Dict[str, Descriptor]:
69
+ @AsyncStatus.wrap
70
+ async def kickoff(self) -> None:
71
+ await self._trigger_logic.kickoff()
72
+
73
+ @AsyncStatus.wrap
74
+ async def complete(self) -> None:
75
+ await self._trigger_logic.complete()
76
+
77
+ async def describe_configuration(self) -> Dict[str, DataKey]:
232
78
  return await merge_gathered_dicts(
233
79
  [sig.describe() for sig in self._configuration_signals]
234
80
  )
@@ -237,63 +83,3 @@ class HardwareTriggeredFlyable(
237
83
  return await merge_gathered_dicts(
238
84
  [sig.read() for sig in self._configuration_signals]
239
85
  )
240
-
241
- async def describe_collect(self) -> Dict[str, Descriptor]:
242
- return self._describe
243
-
244
- @AsyncStatus.wrap
245
- async def kickoff(self) -> None:
246
- self._watchers = []
247
- self._fly_status = AsyncStatus(self._fly(), self._watchers)
248
- self._fly_start = time.monotonic()
249
-
250
- async def _fly(self) -> None:
251
- await self._trigger_logic.start()
252
- # Wait for all detectors to have written up to a particular frame
253
- await self._detector_group_logic.wait_for_index(
254
- self._last_frame - self._offset, timeout=self._trigger_to_frame_timeout
255
- )
256
-
257
- async def collect_asset_docs(self) -> AsyncIterator[Asset]:
258
- current_frame = self._current_frame
259
- stream_datums: List[Asset] = []
260
- async for asset in self._detector_group_logic.collect_asset_docs():
261
- name, doc = asset
262
- if name == "stream_datum":
263
- current_frame = doc["indices"]["stop"] + self._offset
264
- # Defer stream_datums until all stream_resources have been produced
265
- # In a single collect, all the stream_resources are produced first
266
- # followed by their stream_datums
267
- stream_datums.append(asset)
268
- else:
269
- yield asset
270
- for asset in stream_datums:
271
- yield asset
272
- if current_frame != self._current_frame:
273
- self._current_frame = current_frame
274
- for watcher in self._watchers:
275
- watcher(
276
- name=self.name,
277
- current=current_frame,
278
- initial=0,
279
- target=self._last_frame,
280
- unit="",
281
- precision=0,
282
- time_elapsed=time.monotonic() - self._fly_start,
283
- )
284
-
285
- def complete(self) -> AsyncStatus:
286
- assert self._fly_status, "Kickoff not run"
287
- return self._fly_status
288
-
289
- @AsyncStatus.wrap
290
- async def unstage(self) -> None:
291
- await asyncio.gather(
292
- self._trigger_logic.stop(),
293
- self._detector_group_logic.close(),
294
- self._detector_group_logic.disarm(),
295
- )
296
-
297
- @property
298
- def hints(self) -> Hints:
299
- return self._detector_group_logic.hints()
@@ -0,0 +1,82 @@
1
+ import asyncio
2
+ from functools import cached_property
3
+ from typing import Callable, Optional, Type
4
+ from unittest.mock import Mock
5
+
6
+ from bluesky.protocols import Descriptor, Reading
7
+
8
+ from ophyd_async.core.signal_backend import SignalBackend
9
+ from ophyd_async.core.soft_signal_backend import SoftSignalBackend
10
+ from ophyd_async.core.utils import DEFAULT_TIMEOUT, ReadingValueCallback, T
11
+
12
+
13
+ class MockSignalBackend(SignalBackend[T]):
14
+ def __init__(
15
+ self,
16
+ datatype: Optional[Type[T]] = None,
17
+ initial_backend: Optional[SignalBackend[T]] = None,
18
+ ) -> None:
19
+ if isinstance(initial_backend, MockSignalBackend):
20
+ raise ValueError("Cannot make a MockSignalBackend for a MockSignalBackends")
21
+
22
+ self.initial_backend = initial_backend
23
+
24
+ if datatype is None:
25
+ assert (
26
+ self.initial_backend
27
+ ), "Must supply either initial_backend or datatype"
28
+ datatype = self.initial_backend.datatype
29
+
30
+ self.datatype = datatype
31
+
32
+ if not isinstance(self.initial_backend, SoftSignalBackend):
33
+ # If the backend is a hard signal backend, or not provided,
34
+ # then we create a soft signal to mimic it
35
+
36
+ self.soft_backend = SoftSignalBackend(datatype=datatype)
37
+ else:
38
+ self.soft_backend = self.initial_backend
39
+
40
+ def source(self, name: str) -> str:
41
+ if self.initial_backend:
42
+ return f"mock+{self.initial_backend.source(name)}"
43
+ return f"mock+{name}"
44
+
45
+ async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
46
+ pass
47
+
48
+ @cached_property
49
+ def put_mock(self) -> Mock:
50
+ return Mock(name="put", spec=Callable)
51
+
52
+ @cached_property
53
+ def put_proceeds(self) -> asyncio.Event:
54
+ put_proceeds = asyncio.Event()
55
+ put_proceeds.set()
56
+ return put_proceeds
57
+
58
+ async def put(self, value: Optional[T], wait=True, timeout=None):
59
+ self.put_mock(value, wait=wait, timeout=timeout)
60
+ await self.soft_backend.put(value, wait=wait, timeout=timeout)
61
+
62
+ if wait:
63
+ await asyncio.wait_for(self.put_proceeds.wait(), timeout=timeout)
64
+
65
+ def set_value(self, value: T):
66
+ self.soft_backend.set_value(value)
67
+
68
+ async def get_reading(self) -> Reading:
69
+ return await self.soft_backend.get_reading()
70
+
71
+ async def get_value(self) -> T:
72
+ return await self.soft_backend.get_value()
73
+
74
+ async def get_setpoint(self) -> T:
75
+ """For a soft signal, the setpoint and readback values are the same."""
76
+ return await self.soft_backend.get_setpoint()
77
+
78
+ async def get_datakey(self, source: str) -> Descriptor:
79
+ return await self.soft_backend.get_datakey(source)
80
+
81
+ def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
82
+ self.soft_backend.set_callback(callback)