ophyd-async 0.3a1__py3-none-any.whl → 0.3a3__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 (69) hide show
  1. ophyd_async/__init__.py +1 -4
  2. ophyd_async/_version.py +1 -1
  3. ophyd_async/core/__init__.py +23 -3
  4. ophyd_async/core/_providers.py +3 -1
  5. ophyd_async/core/detector.py +72 -46
  6. ophyd_async/core/device.py +8 -0
  7. ophyd_async/core/flyer.py +12 -21
  8. ophyd_async/core/signal.py +134 -20
  9. ophyd_async/core/signal_backend.py +6 -3
  10. ophyd_async/core/sim_signal_backend.py +32 -20
  11. ophyd_async/core/standard_readable.py +212 -23
  12. ophyd_async/core/utils.py +18 -1
  13. ophyd_async/epics/_backend/_aioca.py +17 -15
  14. ophyd_async/epics/_backend/_p4p.py +34 -25
  15. ophyd_async/epics/_backend/common.py +16 -11
  16. ophyd_async/epics/areadetector/__init__.py +8 -0
  17. ophyd_async/epics/areadetector/aravis.py +67 -0
  18. ophyd_async/epics/areadetector/controllers/__init__.py +2 -1
  19. ophyd_async/epics/areadetector/controllers/aravis_controller.py +73 -0
  20. ophyd_async/epics/areadetector/controllers/kinetix_controller.py +49 -0
  21. ophyd_async/epics/areadetector/controllers/pilatus_controller.py +36 -24
  22. ophyd_async/epics/areadetector/controllers/vimba_controller.py +66 -0
  23. ophyd_async/epics/areadetector/drivers/__init__.py +6 -0
  24. ophyd_async/epics/areadetector/drivers/aravis_driver.py +154 -0
  25. ophyd_async/epics/areadetector/drivers/kinetix_driver.py +24 -0
  26. ophyd_async/epics/areadetector/drivers/pilatus_driver.py +4 -4
  27. ophyd_async/epics/areadetector/drivers/vimba_driver.py +58 -0
  28. ophyd_async/epics/areadetector/kinetix.py +46 -0
  29. ophyd_async/epics/areadetector/pilatus.py +45 -0
  30. ophyd_async/epics/areadetector/single_trigger_det.py +14 -6
  31. ophyd_async/epics/areadetector/vimba.py +43 -0
  32. ophyd_async/epics/areadetector/writers/_hdffile.py +4 -4
  33. ophyd_async/epics/areadetector/writers/hdf_writer.py +12 -4
  34. ophyd_async/epics/areadetector/writers/nd_file_hdf.py +1 -0
  35. ophyd_async/epics/demo/__init__.py +45 -18
  36. ophyd_async/epics/motion/motor.py +24 -19
  37. ophyd_async/epics/pvi/__init__.py +3 -0
  38. ophyd_async/epics/pvi/pvi.py +318 -0
  39. ophyd_async/epics/signal/signal.py +26 -9
  40. ophyd_async/log.py +130 -0
  41. ophyd_async/panda/__init__.py +17 -6
  42. ophyd_async/panda/_common_blocks.py +49 -0
  43. ophyd_async/panda/_hdf_panda.py +48 -0
  44. ophyd_async/panda/{panda_controller.py → _panda_controller.py} +3 -7
  45. ophyd_async/panda/_trigger.py +39 -0
  46. ophyd_async/panda/writers/__init__.py +3 -0
  47. ophyd_async/panda/writers/_hdf_writer.py +220 -0
  48. ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
  49. ophyd_async/planstubs/__init__.py +5 -0
  50. ophyd_async/planstubs/prepare_trigger_and_dets.py +57 -0
  51. ophyd_async/protocols.py +96 -0
  52. ophyd_async/sim/__init__.py +11 -0
  53. ophyd_async/sim/demo/__init__.py +3 -0
  54. ophyd_async/sim/demo/sim_motor.py +118 -0
  55. ophyd_async/sim/pattern_generator.py +318 -0
  56. ophyd_async/sim/sim_pattern_detector_control.py +55 -0
  57. ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
  58. ophyd_async/sim/sim_pattern_generator.py +37 -0
  59. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a3.dist-info}/METADATA +30 -69
  60. ophyd_async-0.3a3.dist-info/RECORD +83 -0
  61. ophyd_async/epics/pvi.py +0 -70
  62. ophyd_async/panda/panda.py +0 -241
  63. ophyd_async-0.3a1.dist-info/RECORD +0 -56
  64. /ophyd_async/panda/{table.py → _table.py} +0 -0
  65. /ophyd_async/panda/{utils.py → _utils.py} +0 -0
  66. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a3.dist-info}/LICENSE +0 -0
  67. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a3.dist-info}/WHEEL +0 -0
  68. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a3.dist-info}/entry_points.txt +0 -0
  69. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a3.dist-info}/top_level.txt +0 -0
ophyd_async/__init__.py CHANGED
@@ -1,6 +1,3 @@
1
- from importlib.metadata import version # noqa
2
-
3
- __version__ = version("ophyd-async")
4
- del version
1
+ from ._version import __version__
5
2
 
6
3
  __all__ = ["__version__"]
ophyd_async/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.3a1'
15
+ __version__ = version = '0.3a3'
16
16
  __version_tuple__ = version_tuple = (0, 3)
@@ -6,7 +6,13 @@ from ._providers import (
6
6
  StaticDirectoryProvider,
7
7
  )
8
8
  from .async_status import AsyncStatus
9
- from .detector import DetectorControl, DetectorTrigger, DetectorWriter, StandardDetector
9
+ from .detector import (
10
+ DetectorControl,
11
+ DetectorTrigger,
12
+ DetectorWriter,
13
+ StandardDetector,
14
+ TriggerInfo,
15
+ )
10
16
  from .device import Device, DeviceCollector, DeviceVector
11
17
  from .device_save_loader import (
12
18
  get_signal_values,
@@ -17,23 +23,29 @@ from .device_save_loader import (
17
23
  set_signal_values,
18
24
  walk_rw_signals,
19
25
  )
20
- from .flyer import HardwareTriggeredFlyable, TriggerInfo, TriggerLogic
26
+ from .flyer import HardwareTriggeredFlyable, TriggerLogic
21
27
  from .signal import (
22
28
  Signal,
23
29
  SignalR,
24
30
  SignalRW,
25
31
  SignalW,
26
32
  SignalX,
33
+ assert_configuration,
34
+ assert_emitted,
35
+ assert_reading,
36
+ assert_value,
27
37
  observe_value,
28
38
  set_and_wait_for_value,
29
39
  set_sim_callback,
30
40
  set_sim_put_proceeds,
31
41
  set_sim_value,
42
+ soft_signal_r_and_backend,
43
+ soft_signal_rw,
32
44
  wait_for_value,
33
45
  )
34
46
  from .signal_backend import SignalBackend
35
47
  from .sim_signal_backend import SimSignalBackend
36
- from .standard_readable import StandardReadable
48
+ from .standard_readable import ConfigSignal, HintedSignal, StandardReadable
37
49
  from .utils import (
38
50
  DEFAULT_TIMEOUT,
39
51
  Callback,
@@ -61,6 +73,8 @@ __all__ = [
61
73
  "SignalW",
62
74
  "SignalRW",
63
75
  "SignalX",
76
+ "soft_signal_r_and_backend",
77
+ "soft_signal_rw",
64
78
  "observe_value",
65
79
  "set_and_wait_for_value",
66
80
  "set_sim_callback",
@@ -74,6 +88,8 @@ __all__ = [
74
88
  "ShapeProvider",
75
89
  "StaticDirectoryProvider",
76
90
  "StandardReadable",
91
+ "ConfigSignal",
92
+ "HintedSignal",
77
93
  "TriggerInfo",
78
94
  "TriggerLogic",
79
95
  "HardwareTriggeredFlyable",
@@ -93,4 +109,8 @@ __all__ = [
93
109
  "walk_rw_signals",
94
110
  "load_device",
95
111
  "save_device",
112
+ "assert_reading",
113
+ "assert_value",
114
+ "assert_configuration",
115
+ "assert_emitted",
96
116
  ]
@@ -39,8 +39,10 @@ class StaticDirectoryProvider(DirectoryProvider):
39
39
  directory_path: Union[str, Path],
40
40
  filename_prefix: str = "",
41
41
  filename_suffix: str = "",
42
- resource_dir: Path = Path("."),
42
+ resource_dir: Optional[Path] = None,
43
43
  ) -> None:
44
+ if resource_dir is None:
45
+ resource_dir = Path(".")
44
46
  if isinstance(directory_path, str):
45
47
  directory_path = Path(directory_path)
46
48
  self._directory_info = DirectoryInfo(
@@ -10,6 +10,7 @@ from typing import (
10
10
  AsyncIterator,
11
11
  Callable,
12
12
  Dict,
13
+ Generic,
13
14
  List,
14
15
  Optional,
15
16
  Sequence,
@@ -18,11 +19,9 @@ from typing import (
18
19
 
19
20
  from bluesky.protocols import (
20
21
  Collectable,
21
- Configurable,
22
- Descriptor,
22
+ DataKey,
23
23
  Flyable,
24
24
  Preparable,
25
- Readable,
26
25
  Reading,
27
26
  Stageable,
28
27
  StreamAsset,
@@ -30,15 +29,18 @@ from bluesky.protocols import (
30
29
  WritesStreamAssets,
31
30
  )
32
31
 
32
+ from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
33
+
33
34
  from .async_status import AsyncStatus
34
35
  from .device import Device
35
- from .signal import SignalR
36
36
  from .utils import DEFAULT_TIMEOUT, merge_gathered_dicts
37
37
 
38
38
  T = TypeVar("T")
39
39
 
40
40
 
41
41
  class DetectorTrigger(str, Enum):
42
+ """Type of mechanism for triggering a detector to take frames"""
43
+
42
44
  #: Detector generates internal trigger for given rate
43
45
  internal = "internal"
44
46
  #: Expect a series of arbitrary length trigger signals
@@ -51,6 +53,8 @@ class DetectorTrigger(str, Enum):
51
53
 
52
54
  @dataclass(frozen=True)
53
55
  class TriggerInfo:
56
+ """Minimal set of information required to setup triggering on a detector"""
57
+
54
58
  #: Number of triggers that will be sent
55
59
  num: int
56
60
  #: Sort of triggers that will be sent
@@ -62,6 +66,11 @@ class TriggerInfo:
62
66
 
63
67
 
64
68
  class DetectorControl(ABC):
69
+ """
70
+ Classes implementing this interface should hold the logic for
71
+ arming and disarming a detector
72
+ """
73
+
65
74
  @abstractmethod
66
75
  def get_deadtime(self, exposure: float) -> float:
67
76
  """For a given exposure, how long should the time between exposures be"""
@@ -73,19 +82,34 @@ class DetectorControl(ABC):
73
82
  trigger: DetectorTrigger = DetectorTrigger.internal,
74
83
  exposure: Optional[float] = None,
75
84
  ) -> AsyncStatus:
76
- """Arm the detector and return AsyncStatus.
85
+ """
86
+ Arm detector, do all necessary steps to prepare detector for triggers.
87
+
88
+ Args:
89
+ num: Expected number of frames
90
+ trigger: Type of trigger for which to prepare the detector. Defaults to
91
+ DetectorTrigger.internal.
92
+ exposure: Exposure time with which to set up the detector. Defaults to None
93
+ if not applicable or the detector is expected to use its previously-set
94
+ exposure time.
77
95
 
78
- Awaiting the return value will wait for num frames to be written.
96
+ Returns:
97
+ AsyncStatus: Status representing the arm operation. This function returning
98
+ represents the start of the arm. The returned status completing means
99
+ the detector is now armed.
79
100
  """
80
101
 
81
102
  @abstractmethod
82
103
  async def disarm(self):
83
- """Disarm the detector"""
104
+ """Disarm the detector, return detector to an idle state"""
84
105
 
85
106
 
86
107
  class DetectorWriter(ABC):
108
+ """Logic for making a detector write data to somewhere persistent
109
+ (e.g. an HDF5 file)"""
110
+
87
111
  @abstractmethod
88
- async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]:
112
+ async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
89
113
  """Open writer and wait for it to be ready for data.
90
114
 
91
115
  Args:
@@ -100,7 +124,7 @@ class DetectorWriter(ABC):
100
124
  def observe_indices_written(
101
125
  self, timeout=DEFAULT_TIMEOUT
102
126
  ) -> AsyncGenerator[int, None]:
103
- """Yield each index as it is written"""
127
+ """Yield the index of each frame (or equivalent data point) as it is written"""
104
128
 
105
129
  @abstractmethod
106
130
  async def get_indices_written(self) -> int:
@@ -112,50 +136,51 @@ class DetectorWriter(ABC):
112
136
 
113
137
  @abstractmethod
114
138
  async def close(self) -> None:
115
- """Close writer and wait for it to be finished"""
139
+ """Close writer, blocks until I/O is complete"""
116
140
 
117
141
 
118
142
  class StandardDetector(
119
143
  Device,
120
144
  Stageable,
121
- Configurable,
122
- Readable,
145
+ AsyncConfigurable,
146
+ AsyncReadable,
123
147
  Triggerable,
124
148
  Preparable,
125
149
  Flyable,
126
150
  Collectable,
127
151
  WritesStreamAssets,
152
+ Generic[T],
128
153
  ):
129
- """Detector with useful step and flyscan behaviour.
130
-
131
- Must be supplied instances of classes that inherit from DetectorControl and
132
- DetectorData, to dictate how the detector will be controlled (i.e. arming and
133
- disarming) as well as how the detector data will be written (i.e. opening and
134
- closing the writer, and handling data writing indices).
135
-
154
+ """
155
+ Useful detector base class for step and fly scanning detectors.
156
+ Aggregates controller and writer logic together.
136
157
  """
137
158
 
138
159
  def __init__(
139
160
  self,
140
161
  controller: DetectorControl,
141
162
  writer: DetectorWriter,
142
- config_sigs: Sequence[SignalR] = (),
163
+ config_sigs: Sequence[AsyncReadable] = (),
143
164
  name: str = "",
144
165
  writer_timeout: float = DEFAULT_TIMEOUT,
145
166
  ) -> None:
146
167
  """
147
- Parameters
148
- ----------
149
- control:
150
- instance of class which inherits from :class:`DetectorControl`
151
- data:
152
- instance of class which inherits from :class:`DetectorData`
153
- name:
154
- detector name
168
+ Constructor
169
+
170
+ Args:
171
+ controller: Logic for arming and disarming the detector
172
+ writer: Logic for making the detector write persistent data
173
+ config_sigs: Signals to read when describe and read
174
+ configuration are called. Defaults to ().
175
+ name: Device name. Defaults to "".
176
+ writer_timeout: Timeout for frame writing to start, if the
177
+ timeout is reached, ophyd-async assumes the detector
178
+ has a problem and raises an error.
179
+ Defaults to DEFAULT_TIMEOUT.
155
180
  """
156
181
  self._controller = controller
157
182
  self._writer = writer
158
- self._describe: Dict[str, Descriptor] = {}
183
+ self._describe: Dict[str, DataKey] = {}
159
184
  self._config_sigs = list(config_sigs)
160
185
  self._frame_writing_timeout = writer_timeout
161
186
  # For prepare
@@ -180,15 +205,15 @@ class StandardDetector(
180
205
 
181
206
  @AsyncStatus.wrap
182
207
  async def stage(self) -> None:
183
- """Disarm the detector, stop filewriting, and open file for writing."""
184
- await self.check_config_sigs()
208
+ # Disarm the detector, stop filewriting, and open file for writing.
209
+ await self._check_config_sigs()
185
210
  await asyncio.gather(self.writer.close(), self.controller.disarm())
186
211
  self._describe = await self.writer.open()
187
212
 
188
- async def check_config_sigs(self):
213
+ async def _check_config_sigs(self):
189
214
  """Checks configuration signals are named and connected."""
190
215
  for signal in self._config_sigs:
191
- if signal._name == "":
216
+ if signal.name == "":
192
217
  raise Exception(
193
218
  "config signal must be named before it is passed to the detector"
194
219
  )
@@ -202,26 +227,25 @@ class StandardDetector(
202
227
 
203
228
  @AsyncStatus.wrap
204
229
  async def unstage(self) -> None:
205
- """Stop data writing."""
230
+ # Stop data writing.
206
231
  await self.writer.close()
207
232
 
208
233
  async def read_configuration(self) -> Dict[str, Reading]:
209
234
  return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
210
235
 
211
- async def describe_configuration(self) -> Dict[str, Descriptor]:
236
+ async def describe_configuration(self) -> Dict[str, DataKey]:
212
237
  return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
213
238
 
214
239
  async def read(self) -> Dict[str, Reading]:
215
- """Read the detector"""
216
240
  # All data is in StreamResources, not Events, so nothing to output here
217
241
  return {}
218
242
 
219
- def describe(self) -> Dict[str, Descriptor]:
243
+ async def describe(self) -> Dict[str, DataKey]:
220
244
  return self._describe
221
245
 
222
246
  @AsyncStatus.wrap
223
247
  async def trigger(self) -> None:
224
- """Arm the detector and wait for it to finish."""
248
+ # Arm the detector and wait for it to finish.
225
249
  indices_written = await self.writer.get_indices_written()
226
250
  written_status = await self.controller.arm(
227
251
  num=1,
@@ -240,11 +264,12 @@ class StandardDetector(
240
264
  self,
241
265
  value: T,
242
266
  ) -> AsyncStatus:
243
- """Arm detector"""
267
+ # Just arm detector for the time being
244
268
  return AsyncStatus(self._prepare(value))
245
269
 
246
270
  async def _prepare(self, value: T) -> None:
247
- """Arm detector.
271
+ """
272
+ Arm detector.
248
273
 
249
274
  Prepare the detector with trigger information. This is determined at and passed
250
275
  in from the plan level.
@@ -253,6 +278,9 @@ class StandardDetector(
253
278
  trigger information determined in trigger.
254
279
 
255
280
  To do: Unify prepare to be use for both fly and step scans.
281
+
282
+ Args:
283
+ value: TriggerInfo describing how to trigger the detector
256
284
  """
257
285
  assert type(value) is TriggerInfo
258
286
  self._trigger_info = value
@@ -301,17 +329,15 @@ class StandardDetector(
301
329
  assert self._fly_status, "Kickoff not run"
302
330
  return await self._fly_status
303
331
 
304
- async def describe_collect(self) -> Dict[str, Descriptor]:
332
+ async def describe_collect(self) -> Dict[str, DataKey]:
305
333
  return self._describe
306
334
 
307
335
  async def collect_asset_docs(
308
336
  self, index: Optional[int] = None
309
337
  ) -> AsyncIterator[StreamAsset]:
310
- """Collect stream datum documents for all indices written.
311
-
312
- The index is optional, and provided for flyscans, however this needs to be
313
- retrieved for stepscans.
314
- """
338
+ # Collect stream datum documents for all indices written.
339
+ # The index is optional, and provided for fly scans, however this needs to be
340
+ # retrieved for step scans.
315
341
  if not index:
316
342
  index = await self.writer.get_indices_written()
317
343
  async for doc in self.writer.collect_stream_docs(index):
@@ -82,6 +82,14 @@ VT = TypeVar("VT", bound=Device)
82
82
 
83
83
 
84
84
  class DeviceVector(Dict[int, VT], Device):
85
+ """
86
+ Defines device components with indices.
87
+
88
+ In the below example, foos becomes a dictionary on the parent device
89
+ at runtime, so parent.foos[2] returns a FooDevice. For example usage see
90
+ :class:`~ophyd_async.epics.demo.DynamicSensorGroup`
91
+ """
92
+
85
93
  def children(self) -> Generator[Tuple[str, Device], None, None]:
86
94
  for attr_name, attr in self.items():
87
95
  if isinstance(attr, Device):
ophyd_async/core/flyer.py CHANGED
@@ -1,10 +1,9 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Dict, Generic, Optional, Sequence, TypeVar
2
+ from typing import Dict, Generic, Sequence, TypeVar
3
3
 
4
- from bluesky.protocols import Descriptor, Flyable, Preparable, Reading, Stageable
4
+ from bluesky.protocols import DataKey, Flyable, Preparable, Reading, Stageable
5
5
 
6
6
  from .async_status import AsyncStatus
7
- from .detector import TriggerInfo
8
7
  from .device import Device
9
8
  from .signal import SignalR
10
9
  from .utils import merge_gathered_dicts
@@ -13,18 +12,18 @@ T = TypeVar("T")
13
12
 
14
13
 
15
14
  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
15
  @abstractmethod
21
16
  async def prepare(self, value: T):
22
17
  """Move to the start of the flyscan"""
23
18
 
24
19
  @abstractmethod
25
- async def start(self):
20
+ async def kickoff(self):
26
21
  """Start the flyscan"""
27
22
 
23
+ @abstractmethod
24
+ async def complete(self):
25
+ """Block until the flyscan is done"""
26
+
28
27
  @abstractmethod
29
28
  async def stop(self):
30
29
  """Stop flying and wait everything to be stopped"""
@@ -45,19 +44,12 @@ class HardwareTriggeredFlyable(
45
44
  ):
46
45
  self._trigger_logic = trigger_logic
47
46
  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
47
  super().__init__(name=name)
52
48
 
53
49
  @property
54
50
  def trigger_logic(self) -> TriggerLogic[T]:
55
51
  return self._trigger_logic
56
52
 
57
- @property
58
- def trigger_info(self) -> Optional[TriggerInfo]:
59
- return self._trigger_info
60
-
61
53
  @AsyncStatus.wrap
62
54
  async def stage(self) -> None:
63
55
  await self.unstage()
@@ -71,19 +63,18 @@ class HardwareTriggeredFlyable(
71
63
  return AsyncStatus(self._prepare(value))
72
64
 
73
65
  async def _prepare(self, value: T) -> None:
74
- self._trigger_info = self._trigger_logic.trigger_info(value)
75
66
  # Move to start and setup the flyscan
76
67
  await self._trigger_logic.prepare(value)
77
68
 
78
69
  @AsyncStatus.wrap
79
70
  async def kickoff(self) -> None:
80
- self._fly_status = AsyncStatus(self._trigger_logic.start())
71
+ await self._trigger_logic.kickoff()
81
72
 
82
- def complete(self) -> AsyncStatus:
83
- assert self._fly_status, "Kickoff not run"
84
- return self._fly_status
73
+ @AsyncStatus.wrap
74
+ async def complete(self) -> None:
75
+ await self._trigger_logic.complete()
85
76
 
86
- async def describe_configuration(self) -> Dict[str, Descriptor]:
77
+ async def describe_configuration(self) -> Dict[str, DataKey]:
87
78
  return await merge_gathered_dicts(
88
79
  [sig.describe() for sig in self._configuration_signals]
89
80
  )
@@ -2,19 +2,30 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import functools
5
- from typing import AsyncGenerator, Callable, Dict, Generic, Optional, Union
5
+ from typing import (
6
+ Any,
7
+ AsyncGenerator,
8
+ Callable,
9
+ Dict,
10
+ Generic,
11
+ Mapping,
12
+ Optional,
13
+ Tuple,
14
+ Type,
15
+ Union,
16
+ )
6
17
 
7
18
  from bluesky.protocols import (
8
- Descriptor,
19
+ DataKey,
9
20
  Locatable,
10
21
  Location,
11
22
  Movable,
12
- Readable,
13
23
  Reading,
14
- Stageable,
15
24
  Subscribable,
16
25
  )
17
26
 
27
+ from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageable
28
+
18
29
  from .async_status import AsyncStatus
19
30
  from .device import Device
20
31
  from .signal_backend import SignalBackend
@@ -45,24 +56,18 @@ class Signal(Device, Generic[T]):
45
56
  """A Device with the concept of a value, with R, RW, W and X flavours"""
46
57
 
47
58
  def __init__(
48
- self, backend: SignalBackend[T], timeout: Optional[float] = DEFAULT_TIMEOUT
59
+ self,
60
+ backend: SignalBackend[T],
61
+ timeout: Optional[float] = DEFAULT_TIMEOUT,
62
+ name: str = "",
49
63
  ) -> None:
50
- self._name = ""
64
+ super().__init__(name)
51
65
  self._timeout = timeout
52
66
  self._init_backend = self._backend = backend
53
67
 
54
- @property
55
- def name(self) -> str:
56
- return self._name
57
-
58
- def set_name(self, name: str = ""):
59
- self._name = name
60
-
61
68
  async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT):
62
69
  if sim:
63
- self._backend = SimSignalBackend(
64
- datatype=self._init_backend.datatype, source=self._init_backend.source
65
- )
70
+ self._backend = SimSignalBackend(datatype=self._init_backend.datatype)
66
71
  _sim_backends[self] = self._backend
67
72
  else:
68
73
  self._backend = self._init_backend
@@ -72,7 +77,7 @@ class Signal(Device, Generic[T]):
72
77
  @property
73
78
  def source(self) -> str:
74
79
  """Like ca://PV_PREFIX:SIGNAL, or "" if not set"""
75
- return self._backend.source
80
+ return self._backend.source(self.name)
76
81
 
77
82
  __lt__ = __le__ = __eq__ = __ge__ = __gt__ = __ne__ = _fail
78
83
 
@@ -133,7 +138,7 @@ class _SignalCache(Generic[T]):
133
138
  return self._staged or bool(self._listeners)
134
139
 
135
140
 
136
- class SignalR(Signal[T], Readable, Stageable, Subscribable):
141
+ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
137
142
  """Signal that can be read from and monitored"""
138
143
 
139
144
  _cache: Optional[_SignalCache] = None
@@ -166,9 +171,9 @@ class SignalR(Signal[T], Readable, Stageable, Subscribable):
166
171
  return {self.name: await self._backend_or_cache(cached).get_reading()}
167
172
 
168
173
  @_add_timeout
169
- async def describe(self) -> Dict[str, Descriptor]:
174
+ async def describe(self) -> Dict[str, DataKey]:
170
175
  """Return a single item dict with the descriptor in it"""
171
- return {self.name: await self._backend.get_descriptor()}
176
+ return {self.name: await self._backend.get_datakey(self.source)}
172
177
 
173
178
  @_add_timeout
174
179
  async def get_value(self, cached: Optional[bool] = None) -> T:
@@ -253,6 +258,115 @@ def set_sim_callback(signal: Signal[T], callback: ReadingValueCallback[T]) -> No
253
258
  return _sim_backends[signal].set_callback(callback)
254
259
 
255
260
 
261
+ def soft_signal_rw(
262
+ datatype: Optional[Type[T]] = None,
263
+ initial_value: Optional[T] = None,
264
+ name: str = "",
265
+ ) -> SignalRW[T]:
266
+ """Creates a read-writable Signal with a SimSignalBackend"""
267
+ signal = SignalRW(SimSignalBackend(datatype, initial_value), name=name)
268
+ return signal
269
+
270
+
271
+ def soft_signal_r_and_backend(
272
+ datatype: Optional[Type[T]] = None,
273
+ initial_value: Optional[T] = None,
274
+ name: str = "",
275
+ ) -> Tuple[SignalR[T], SimSignalBackend]:
276
+ """Returns a tuple of a read-only Signal and its SimSignalBackend through
277
+ which the signal can be internally modified within the device. Use
278
+ soft_signal_rw if you want a device that is externally modifiable
279
+ """
280
+ backend = SimSignalBackend(datatype, initial_value)
281
+ signal = SignalR(backend, name=name)
282
+ return (signal, backend)
283
+
284
+
285
+ async def assert_value(signal: SignalR[T], value: Any) -> None:
286
+ """Assert a signal's value and compare it an expected signal.
287
+
288
+ Parameters
289
+ ----------
290
+ signal:
291
+ signal with get_value.
292
+ value:
293
+ The expected value from the signal.
294
+
295
+ Notes
296
+ -----
297
+ Example usage::
298
+ await assert_value(signal, value)
299
+
300
+ """
301
+ assert await signal.get_value() == value
302
+
303
+
304
+ async def assert_reading(
305
+ readable: AsyncReadable, reading: Mapping[str, Reading]
306
+ ) -> None:
307
+ """Assert readings from readable.
308
+
309
+ Parameters
310
+ ----------
311
+ readable:
312
+ Callable with readable.read function that generate readings.
313
+
314
+ reading:
315
+ The expected readings from the readable.
316
+
317
+ Notes
318
+ -----
319
+ Example usage::
320
+ await assert_reading(readable, reading)
321
+
322
+ """
323
+ assert await readable.read() == reading
324
+
325
+
326
+ async def assert_configuration(
327
+ configurable: AsyncConfigurable,
328
+ configuration: Mapping[str, Reading],
329
+ ) -> None:
330
+ """Assert readings from Configurable.
331
+
332
+ Parameters
333
+ ----------
334
+ configurable:
335
+ Configurable with Configurable.read function that generate readings.
336
+
337
+ configuration:
338
+ The expected readings from configurable.
339
+
340
+ Notes
341
+ -----
342
+ Example usage::
343
+ await assert_configuration(configurable configuration)
344
+
345
+ """
346
+ assert await configurable.read_configuration() == configuration
347
+
348
+
349
+ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
350
+ """Assert emitted document generated by running a Bluesky plan
351
+
352
+ Parameters
353
+ ----------
354
+ Doc:
355
+ A dictionary
356
+
357
+ numbers:
358
+ expected emission in kwarg from
359
+
360
+ Notes
361
+ -----
362
+ Example usage::
363
+ assert_emitted(docs, start=1, descriptor=1,
364
+ resource=1, datum=1, event=1, stop=1)
365
+ """
366
+ assert list(docs) == list(numbers)
367
+ assert {name: len(d) for name, d in docs.items()} == numbers
368
+
369
+
256
370
  async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, None]:
257
371
  """Subscribe to the value of a signal so it can be iterated from.
258
372