ophyd-async 0.3a1__py3-none-any.whl → 0.3a2__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 (58) hide show
  1. ophyd_async/__init__.py +1 -4
  2. ophyd_async/_version.py +1 -1
  3. ophyd_async/core/__init__.py +12 -2
  4. ophyd_async/core/_providers.py +3 -1
  5. ophyd_async/core/detector.py +65 -38
  6. ophyd_async/core/device.py +8 -0
  7. ophyd_async/core/flyer.py +10 -19
  8. ophyd_async/core/signal.py +36 -17
  9. ophyd_async/core/signal_backend.py +5 -2
  10. ophyd_async/core/sim_signal_backend.py +28 -16
  11. ophyd_async/core/standard_readable.py +4 -2
  12. ophyd_async/core/utils.py +18 -1
  13. ophyd_async/epics/_backend/_aioca.py +13 -11
  14. ophyd_async/epics/_backend/_p4p.py +19 -16
  15. ophyd_async/epics/_backend/common.py +16 -11
  16. ophyd_async/epics/areadetector/__init__.py +4 -0
  17. ophyd_async/epics/areadetector/aravis.py +69 -0
  18. ophyd_async/epics/areadetector/controllers/aravis_controller.py +73 -0
  19. ophyd_async/epics/areadetector/controllers/pilatus_controller.py +36 -24
  20. ophyd_async/epics/areadetector/drivers/aravis_driver.py +154 -0
  21. ophyd_async/epics/areadetector/drivers/pilatus_driver.py +4 -4
  22. ophyd_async/epics/areadetector/pilatus.py +50 -0
  23. ophyd_async/epics/areadetector/writers/_hdffile.py +4 -4
  24. ophyd_async/epics/areadetector/writers/hdf_writer.py +6 -1
  25. ophyd_async/epics/demo/__init__.py +33 -3
  26. ophyd_async/epics/motion/motor.py +20 -14
  27. ophyd_async/epics/pvi/__init__.py +3 -0
  28. ophyd_async/epics/pvi/pvi.py +318 -0
  29. ophyd_async/epics/signal/signal.py +26 -9
  30. ophyd_async/panda/__init__.py +17 -6
  31. ophyd_async/panda/_common_blocks.py +49 -0
  32. ophyd_async/panda/_hdf_panda.py +48 -0
  33. ophyd_async/panda/{panda_controller.py → _panda_controller.py} +3 -7
  34. ophyd_async/panda/_trigger.py +39 -0
  35. ophyd_async/panda/writers/__init__.py +3 -0
  36. ophyd_async/panda/writers/_hdf_writer.py +220 -0
  37. ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
  38. ophyd_async/planstubs/__init__.py +5 -0
  39. ophyd_async/planstubs/prepare_trigger_and_dets.py +57 -0
  40. ophyd_async/protocols.py +73 -0
  41. ophyd_async/sim/__init__.py +11 -0
  42. ophyd_async/sim/demo/__init__.py +3 -0
  43. ophyd_async/sim/demo/sim_motor.py +116 -0
  44. ophyd_async/sim/pattern_generator.py +318 -0
  45. ophyd_async/sim/sim_pattern_detector_control.py +55 -0
  46. ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
  47. ophyd_async/sim/sim_pattern_generator.py +37 -0
  48. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/METADATA +19 -75
  49. ophyd_async-0.3a2.dist-info/RECORD +76 -0
  50. ophyd_async/epics/pvi.py +0 -70
  51. ophyd_async/panda/panda.py +0 -241
  52. ophyd_async-0.3a1.dist-info/RECORD +0 -56
  53. /ophyd_async/panda/{table.py → _table.py} +0 -0
  54. /ophyd_async/panda/{utils.py → _utils.py} +0 -0
  55. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/LICENSE +0 -0
  56. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/WHEEL +0 -0
  57. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/entry_points.txt +0 -0
  58. {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.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.3a2'
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,7 +23,7 @@ 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,
@@ -29,6 +35,8 @@ from .signal import (
29
35
  set_sim_callback,
30
36
  set_sim_put_proceeds,
31
37
  set_sim_value,
38
+ soft_signal_r_and_backend,
39
+ soft_signal_rw,
32
40
  wait_for_value,
33
41
  )
34
42
  from .signal_backend import SignalBackend
@@ -61,6 +69,8 @@ __all__ = [
61
69
  "SignalW",
62
70
  "SignalRW",
63
71
  "SignalX",
72
+ "soft_signal_r_and_backend",
73
+ "soft_signal_rw",
64
74
  "observe_value",
65
75
  "set_and_wait_for_value",
66
76
  "set_sim_callback",
@@ -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
22
  Descriptor,
23
23
  Flyable,
24
24
  Preparable,
25
- Readable,
26
25
  Reading,
27
26
  Stageable,
28
27
  StreamAsset,
@@ -30,6 +29,8 @@ 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
36
  from .signal import SignalR
@@ -39,6 +40,8 @@ T = TypeVar("T")
39
40
 
40
41
 
41
42
  class DetectorTrigger(str, Enum):
43
+ """Type of mechanism for triggering a detector to take frames"""
44
+
42
45
  #: Detector generates internal trigger for given rate
43
46
  internal = "internal"
44
47
  #: Expect a series of arbitrary length trigger signals
@@ -51,6 +54,8 @@ class DetectorTrigger(str, Enum):
51
54
 
52
55
  @dataclass(frozen=True)
53
56
  class TriggerInfo:
57
+ """Minimal set of information required to setup triggering on a detector"""
58
+
54
59
  #: Number of triggers that will be sent
55
60
  num: int
56
61
  #: Sort of triggers that will be sent
@@ -62,6 +67,11 @@ class TriggerInfo:
62
67
 
63
68
 
64
69
  class DetectorControl(ABC):
70
+ """
71
+ Classes implementing this interface should hold the logic for
72
+ arming and disarming a detector
73
+ """
74
+
65
75
  @abstractmethod
66
76
  def get_deadtime(self, exposure: float) -> float:
67
77
  """For a given exposure, how long should the time between exposures be"""
@@ -73,17 +83,32 @@ class DetectorControl(ABC):
73
83
  trigger: DetectorTrigger = DetectorTrigger.internal,
74
84
  exposure: Optional[float] = None,
75
85
  ) -> AsyncStatus:
76
- """Arm the detector and return AsyncStatus.
86
+ """
87
+ Arm detector, do all necessary steps to prepare detector for triggers.
88
+
89
+ Args:
90
+ num: Expected number of frames
91
+ trigger: Type of trigger for which to prepare the detector. Defaults to
92
+ DetectorTrigger.internal.
93
+ exposure: Exposure time with which to set up the detector. Defaults to None
94
+ if not applicable or the detector is expected to use its previously-set
95
+ exposure time.
77
96
 
78
- Awaiting the return value will wait for num frames to be written.
97
+ Returns:
98
+ AsyncStatus: Status representing the arm operation. This function returning
99
+ represents the start of the arm. The returned status completing means
100
+ the detector is now armed.
79
101
  """
80
102
 
81
103
  @abstractmethod
82
104
  async def disarm(self):
83
- """Disarm the detector"""
105
+ """Disarm the detector, return detector to an idle state"""
84
106
 
85
107
 
86
108
  class DetectorWriter(ABC):
109
+ """Logic for making a detector write data to somewhere persistent
110
+ (e.g. an HDF5 file)"""
111
+
87
112
  @abstractmethod
88
113
  async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]:
89
114
  """Open writer and wait for it to be ready for data.
@@ -100,7 +125,7 @@ class DetectorWriter(ABC):
100
125
  def observe_indices_written(
101
126
  self, timeout=DEFAULT_TIMEOUT
102
127
  ) -> AsyncGenerator[int, None]:
103
- """Yield each index as it is written"""
128
+ """Yield the index of each frame (or equivalent data point) as it is written"""
104
129
 
105
130
  @abstractmethod
106
131
  async def get_indices_written(self) -> int:
@@ -112,27 +137,24 @@ class DetectorWriter(ABC):
112
137
 
113
138
  @abstractmethod
114
139
  async def close(self) -> None:
115
- """Close writer and wait for it to be finished"""
140
+ """Close writer, blocks until I/O is complete"""
116
141
 
117
142
 
118
143
  class StandardDetector(
119
144
  Device,
120
145
  Stageable,
121
- Configurable,
122
- Readable,
146
+ AsyncConfigurable,
147
+ AsyncReadable,
123
148
  Triggerable,
124
149
  Preparable,
125
150
  Flyable,
126
151
  Collectable,
127
152
  WritesStreamAssets,
153
+ Generic[T],
128
154
  ):
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
-
155
+ """
156
+ Useful detector base class for step and fly scanning detectors.
157
+ Aggregates controller and writer logic together.
136
158
  """
137
159
 
138
160
  def __init__(
@@ -144,14 +166,18 @@ class StandardDetector(
144
166
  writer_timeout: float = DEFAULT_TIMEOUT,
145
167
  ) -> None:
146
168
  """
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
169
+ Constructor
170
+
171
+ Args:
172
+ controller: Logic for arming and disarming the detector
173
+ writer: Logic for making the detector write persistent data
174
+ config_sigs: Signals to read when describe and read
175
+ configuration are called. Defaults to ().
176
+ name: Device name. Defaults to "".
177
+ writer_timeout: Timeout for frame writing to start, if the
178
+ timeout is reached, ophyd-async assumes the detector
179
+ has a problem and raises an error.
180
+ Defaults to DEFAULT_TIMEOUT.
155
181
  """
156
182
  self._controller = controller
157
183
  self._writer = writer
@@ -180,12 +206,12 @@ class StandardDetector(
180
206
 
181
207
  @AsyncStatus.wrap
182
208
  async def stage(self) -> None:
183
- """Disarm the detector, stop filewriting, and open file for writing."""
184
- await self.check_config_sigs()
209
+ # Disarm the detector, stop filewriting, and open file for writing.
210
+ await self._check_config_sigs()
185
211
  await asyncio.gather(self.writer.close(), self.controller.disarm())
186
212
  self._describe = await self.writer.open()
187
213
 
188
- async def check_config_sigs(self):
214
+ async def _check_config_sigs(self):
189
215
  """Checks configuration signals are named and connected."""
190
216
  for signal in self._config_sigs:
191
217
  if signal._name == "":
@@ -202,7 +228,7 @@ class StandardDetector(
202
228
 
203
229
  @AsyncStatus.wrap
204
230
  async def unstage(self) -> None:
205
- """Stop data writing."""
231
+ # Stop data writing.
206
232
  await self.writer.close()
207
233
 
208
234
  async def read_configuration(self) -> Dict[str, Reading]:
@@ -212,16 +238,15 @@ class StandardDetector(
212
238
  return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
213
239
 
214
240
  async def read(self) -> Dict[str, Reading]:
215
- """Read the detector"""
216
241
  # All data is in StreamResources, not Events, so nothing to output here
217
242
  return {}
218
243
 
219
- def describe(self) -> Dict[str, Descriptor]:
244
+ async def describe(self) -> Dict[str, Descriptor]:
220
245
  return self._describe
221
246
 
222
247
  @AsyncStatus.wrap
223
248
  async def trigger(self) -> None:
224
- """Arm the detector and wait for it to finish."""
249
+ # Arm the detector and wait for it to finish.
225
250
  indices_written = await self.writer.get_indices_written()
226
251
  written_status = await self.controller.arm(
227
252
  num=1,
@@ -240,11 +265,12 @@ class StandardDetector(
240
265
  self,
241
266
  value: T,
242
267
  ) -> AsyncStatus:
243
- """Arm detector"""
268
+ # Just arm detector for the time being
244
269
  return AsyncStatus(self._prepare(value))
245
270
 
246
271
  async def _prepare(self, value: T) -> None:
247
- """Arm detector.
272
+ """
273
+ Arm detector.
248
274
 
249
275
  Prepare the detector with trigger information. This is determined at and passed
250
276
  in from the plan level.
@@ -253,6 +279,9 @@ class StandardDetector(
253
279
  trigger information determined in trigger.
254
280
 
255
281
  To do: Unify prepare to be use for both fly and step scans.
282
+
283
+ Args:
284
+ value: TriggerInfo describing how to trigger the detector
256
285
  """
257
286
  assert type(value) is TriggerInfo
258
287
  self._trigger_info = value
@@ -307,11 +336,9 @@ class StandardDetector(
307
336
  async def collect_asset_docs(
308
337
  self, index: Optional[int] = None
309
338
  ) -> 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
- """
339
+ # Collect stream datum documents for all indices written.
340
+ # The index is optional, and provided for fly scans, however this needs to be
341
+ # retrieved for step scans.
315
342
  if not index:
316
343
  index = await self.writer.get_indices_written()
317
344
  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
4
  from bluesky.protocols import Descriptor, 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,17 +63,16 @@ 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
77
  async def describe_configuration(self) -> Dict[str, Descriptor]:
87
78
  return await merge_gathered_dicts(
@@ -2,19 +2,20 @@ 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 AsyncGenerator, Callable, Dict, Generic, Optional, Tuple, Type, Union
6
6
 
7
7
  from bluesky.protocols import (
8
8
  Descriptor,
9
9
  Locatable,
10
10
  Location,
11
11
  Movable,
12
- Readable,
13
12
  Reading,
14
13
  Stageable,
15
14
  Subscribable,
16
15
  )
17
16
 
17
+ from ophyd_async.protocols import AsyncReadable
18
+
18
19
  from .async_status import AsyncStatus
19
20
  from .device import Device
20
21
  from .signal_backend import SignalBackend
@@ -45,24 +46,18 @@ class Signal(Device, Generic[T]):
45
46
  """A Device with the concept of a value, with R, RW, W and X flavours"""
46
47
 
47
48
  def __init__(
48
- self, backend: SignalBackend[T], timeout: Optional[float] = DEFAULT_TIMEOUT
49
+ self,
50
+ backend: SignalBackend[T],
51
+ timeout: Optional[float] = DEFAULT_TIMEOUT,
52
+ name: str = "",
49
53
  ) -> None:
50
- self._name = ""
54
+ super().__init__(name)
51
55
  self._timeout = timeout
52
56
  self._init_backend = self._backend = backend
53
57
 
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
58
  async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT):
62
59
  if sim:
63
- self._backend = SimSignalBackend(
64
- datatype=self._init_backend.datatype, source=self._init_backend.source
65
- )
60
+ self._backend = SimSignalBackend(datatype=self._init_backend.datatype)
66
61
  _sim_backends[self] = self._backend
67
62
  else:
68
63
  self._backend = self._init_backend
@@ -72,7 +67,7 @@ class Signal(Device, Generic[T]):
72
67
  @property
73
68
  def source(self) -> str:
74
69
  """Like ca://PV_PREFIX:SIGNAL, or "" if not set"""
75
- return self._backend.source
70
+ return self._backend.source(self.name)
76
71
 
77
72
  __lt__ = __le__ = __eq__ = __ge__ = __gt__ = __ne__ = _fail
78
73
 
@@ -133,7 +128,7 @@ class _SignalCache(Generic[T]):
133
128
  return self._staged or bool(self._listeners)
134
129
 
135
130
 
136
- class SignalR(Signal[T], Readable, Stageable, Subscribable):
131
+ class SignalR(Signal[T], AsyncReadable, Stageable, Subscribable):
137
132
  """Signal that can be read from and monitored"""
138
133
 
139
134
  _cache: Optional[_SignalCache] = None
@@ -168,7 +163,7 @@ class SignalR(Signal[T], Readable, Stageable, Subscribable):
168
163
  @_add_timeout
169
164
  async def describe(self) -> Dict[str, Descriptor]:
170
165
  """Return a single item dict with the descriptor in it"""
171
- return {self.name: await self._backend.get_descriptor()}
166
+ return {self.name: await self._backend.get_descriptor(self.source)}
172
167
 
173
168
  @_add_timeout
174
169
  async def get_value(self, cached: Optional[bool] = None) -> T:
@@ -253,6 +248,30 @@ def set_sim_callback(signal: Signal[T], callback: ReadingValueCallback[T]) -> No
253
248
  return _sim_backends[signal].set_callback(callback)
254
249
 
255
250
 
251
+ def soft_signal_rw(
252
+ datatype: Optional[Type[T]] = None,
253
+ initial_value: Optional[T] = None,
254
+ name: str = "",
255
+ ) -> SignalRW[T]:
256
+ """Creates a read-writable Signal with a SimSignalBackend"""
257
+ signal = SignalRW(SimSignalBackend(datatype, initial_value), name=name)
258
+ return signal
259
+
260
+
261
+ def soft_signal_r_and_backend(
262
+ datatype: Optional[Type[T]] = None,
263
+ initial_value: Optional[T] = None,
264
+ name: str = "",
265
+ ) -> Tuple[SignalR[T], SimSignalBackend]:
266
+ """Returns a tuple of a read-only Signal and its SimSignalBackend through
267
+ which the signal can be internally modified within the device. Use
268
+ soft_signal_rw if you want a device that is externally modifiable
269
+ """
270
+ backend = SimSignalBackend(datatype, initial_value)
271
+ signal = SignalR(backend, name=name)
272
+ return (signal, backend)
273
+
274
+
256
275
  async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, None]:
257
276
  """Subscribe to the value of a signal so it can be iterated from.
258
277
 
@@ -13,7 +13,10 @@ class SignalBackend(Generic[T]):
13
13
  datatype: Optional[Type[T]] = None
14
14
 
15
15
  #: Like ca://PV_PREFIX:SIGNAL
16
- source: str = ""
16
+ @abstractmethod
17
+ def source(name: str) -> str:
18
+ """Return source of signal. Signals may pass a name to the backend, which can be
19
+ used or discarded."""
17
20
 
18
21
  @abstractmethod
19
22
  async def connect(self, timeout: float = DEFAULT_TIMEOUT):
@@ -24,7 +27,7 @@ class SignalBackend(Generic[T]):
24
27
  """Put a value to the PV, if wait then wait for completion for up to timeout"""
25
28
 
26
29
  @abstractmethod
27
- async def get_descriptor(self) -> Descriptor:
30
+ async def get_descriptor(self, source: str) -> Descriptor:
28
31
  """Metadata like source, dtype, shape, precision, units"""
29
32
 
30
33
  @abstractmethod
@@ -2,13 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import inspect
5
- import re
6
5
  import time
7
6
  from collections import abc
8
7
  from dataclasses import dataclass
9
8
  from enum import Enum
10
9
  from typing import Any, Dict, Generic, Optional, Type, Union, cast, get_origin
11
10
 
11
+ import numpy as np
12
12
  from bluesky.protocols import Descriptor, Dtype, Reading
13
13
 
14
14
  from .signal_backend import SignalBackend
@@ -37,11 +37,16 @@ class SimConverter(Generic[T]):
37
37
  )
38
38
 
39
39
  def descriptor(self, source: str, value) -> Descriptor:
40
+ dtype = type(value)
41
+ if np.issubdtype(dtype, np.integer):
42
+ dtype = int
43
+ elif np.issubdtype(dtype, np.floating):
44
+ dtype = float
40
45
  assert (
41
- type(value) in primitive_dtypes
46
+ dtype in primitive_dtypes
42
47
  ), f"invalid converter for value of type {type(value)}"
43
- dtype = primitive_dtypes[type(value)]
44
- return dict(source=source, dtype=dtype, shape=[])
48
+ dtype_name = primitive_dtypes[dtype]
49
+ return {"source": source, "dtype": dtype_name, "shape": []}
45
50
 
46
51
  def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
47
52
  if datatype is None:
@@ -52,7 +57,7 @@ class SimConverter(Generic[T]):
52
57
 
53
58
  class SimArrayConverter(SimConverter):
54
59
  def descriptor(self, source: str, value) -> Descriptor:
55
- return dict(source=source, dtype="array", shape=[len(value)])
60
+ return {"source": source, "dtype": "array", "shape": [len(value)]}
56
61
 
57
62
  def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
58
63
  if datatype is None:
@@ -76,9 +81,7 @@ class SimEnumConverter(SimConverter):
76
81
 
77
82
  def descriptor(self, source: str, value) -> Descriptor:
78
83
  choices = [e.value for e in self.enum_class]
79
- return dict(
80
- source=source, dtype="string", shape=[], choices=choices
81
- ) # type: ignore
84
+ return {"source": source, "dtype": "string", "shape": [], "choices": choices} # type: ignore
82
85
 
83
86
  def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
84
87
  if datatype is None:
@@ -109,23 +112,32 @@ class SimSignalBackend(SignalBackend[T]):
109
112
  """An simulated backend to a Signal, created with ``Signal.connect(sim=True)``"""
110
113
 
111
114
  _value: T
112
- _initial_value: T
115
+ _initial_value: Optional[T]
113
116
  _timestamp: float
114
117
  _severity: int
115
118
 
116
- def __init__(self, datatype: Optional[Type[T]], source: str) -> None:
117
- pv = re.split(r"://", source)[-1]
118
- self.source = f"sim://{pv}"
119
+ def __init__(
120
+ self,
121
+ datatype: Optional[Type[T]],
122
+ initial_value: Optional[T] = None,
123
+ ) -> None:
119
124
  self.datatype = datatype
120
- self.pv = source
121
125
  self.converter: SimConverter = DisconnectedSimConverter()
126
+ self._initial_value = initial_value
122
127
  self.put_proceeds = asyncio.Event()
123
128
  self.put_proceeds.set()
124
129
  self.callback: Optional[ReadingValueCallback[T]] = None
125
130
 
131
+ def source(self, name: str) -> str:
132
+ return f"soft://{name}"
133
+
126
134
  async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
127
135
  self.converter = make_converter(self.datatype)
128
- self._initial_value = self.converter.make_initial_value(self.datatype)
136
+ if self._initial_value is None:
137
+ self._initial_value = self.converter.make_initial_value(self.datatype)
138
+ else:
139
+ # convert potentially unconverted initial value passed to init method
140
+ self._initial_value = self.converter.write_value(self._initial_value)
129
141
  self._severity = 0
130
142
 
131
143
  await self.put(None)
@@ -152,8 +164,8 @@ class SimSignalBackend(SignalBackend[T]):
152
164
  if self.callback:
153
165
  self.callback(reading, self._value)
154
166
 
155
- async def get_descriptor(self) -> Descriptor:
156
- return self.converter.descriptor(self.source, self._value)
167
+ async def get_descriptor(self, source: str) -> Descriptor:
168
+ return self.converter.descriptor(source, self._value)
157
169
 
158
170
  async def get_reading(self) -> Reading:
159
171
  return self.converter.reading(self._value, self._timestamp, self._severity)
@@ -1,6 +1,8 @@
1
1
  from typing import Dict, Sequence, Tuple
2
2
 
3
- from bluesky.protocols import Configurable, Descriptor, Readable, Reading, Stageable
3
+ from bluesky.protocols import Descriptor, Reading, Stageable
4
+
5
+ from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
4
6
 
5
7
  from .async_status import AsyncStatus
6
8
  from .device import Device
@@ -8,7 +10,7 @@ from .signal import SignalR
8
10
  from .utils import merge_gathered_dicts
9
11
 
10
12
 
11
- class StandardReadable(Device, Readable, Configurable, Stageable):
13
+ class StandardReadable(Device, AsyncReadable, AsyncConfigurable, Stageable):
12
14
  """Device that owns its children and provides useful default behavior.
13
15
 
14
16
  - When its name is set it renames child Devices