ophyd-async 0.3a3__py3-none-any.whl → 0.3a5__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 (40) hide show
  1. ophyd_async/_version.py +1 -1
  2. ophyd_async/core/__init__.py +26 -11
  3. ophyd_async/core/async_status.py +96 -33
  4. ophyd_async/core/detector.py +22 -28
  5. ophyd_async/core/device.py +50 -14
  6. ophyd_async/core/mock_signal_backend.py +85 -0
  7. ophyd_async/core/mock_signal_utils.py +149 -0
  8. ophyd_async/core/signal.py +93 -53
  9. ophyd_async/core/{sim_signal_backend.py → soft_signal_backend.py} +23 -33
  10. ophyd_async/core/utils.py +19 -0
  11. ophyd_async/epics/_backend/_aioca.py +11 -7
  12. ophyd_async/epics/_backend/_p4p.py +11 -7
  13. ophyd_async/epics/_backend/common.py +17 -17
  14. ophyd_async/epics/areadetector/__init__.py +0 -4
  15. ophyd_async/epics/areadetector/aravis.py +1 -5
  16. ophyd_async/epics/areadetector/controllers/aravis_controller.py +6 -1
  17. ophyd_async/epics/areadetector/drivers/ad_base.py +12 -10
  18. ophyd_async/epics/areadetector/drivers/aravis_driver.py +6 -122
  19. ophyd_async/epics/areadetector/drivers/kinetix_driver.py +7 -4
  20. ophyd_async/epics/areadetector/drivers/pilatus_driver.py +5 -2
  21. ophyd_async/epics/areadetector/drivers/vimba_driver.py +12 -7
  22. ophyd_async/epics/areadetector/utils.py +2 -12
  23. ophyd_async/epics/areadetector/writers/nd_file_hdf.py +21 -19
  24. ophyd_async/epics/areadetector/writers/nd_plugin.py +6 -7
  25. ophyd_async/epics/demo/__init__.py +27 -32
  26. ophyd_async/epics/motion/motor.py +40 -37
  27. ophyd_async/epics/pvi/pvi.py +13 -13
  28. ophyd_async/epics/signal/__init__.py +8 -1
  29. ophyd_async/panda/_hdf_panda.py +3 -3
  30. ophyd_async/planstubs/__init__.py +5 -1
  31. ophyd_async/planstubs/ensure_connected.py +22 -0
  32. ophyd_async/protocols.py +32 -2
  33. ophyd_async/sim/demo/sim_motor.py +50 -35
  34. ophyd_async/sim/pattern_generator.py +5 -5
  35. {ophyd_async-0.3a3.dist-info → ophyd_async-0.3a5.dist-info}/METADATA +2 -2
  36. {ophyd_async-0.3a3.dist-info → ophyd_async-0.3a5.dist-info}/RECORD +40 -37
  37. {ophyd_async-0.3a3.dist-info → ophyd_async-0.3a5.dist-info}/LICENSE +0 -0
  38. {ophyd_async-0.3a3.dist-info → ophyd_async-0.3a5.dist-info}/WHEEL +0 -0
  39. {ophyd_async-0.3a3.dist-info → ophyd_async-0.3a5.dist-info}/entry_points.txt +0 -0
  40. {ophyd_async-0.3a3.dist-info → ophyd_async-0.3a5.dist-info}/top_level.txt +0 -0
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.3a3'
15
+ __version__ = version = '0.3a5'
16
16
  __version_tuple__ = version_tuple = (0, 3)
@@ -5,7 +5,7 @@ from ._providers import (
5
5
  ShapeProvider,
6
6
  StaticDirectoryProvider,
7
7
  )
8
- from .async_status import AsyncStatus
8
+ from .async_status import AsyncStatus, WatchableAsyncStatus
9
9
  from .detector import (
10
10
  DetectorControl,
11
11
  DetectorTrigger,
@@ -24,6 +24,18 @@ from .device_save_loader import (
24
24
  walk_rw_signals,
25
25
  )
26
26
  from .flyer import HardwareTriggeredFlyable, TriggerLogic
27
+ from .mock_signal_backend import (
28
+ MockSignalBackend,
29
+ )
30
+ from .mock_signal_utils import (
31
+ assert_mock_put_called_with,
32
+ callback_on_mock_put,
33
+ mock_puts_blocked,
34
+ reset_mock_put_calls,
35
+ set_mock_put_proceeds,
36
+ set_mock_value,
37
+ set_mock_values,
38
+ )
27
39
  from .signal import (
28
40
  Signal,
29
41
  SignalR,
@@ -36,15 +48,12 @@ from .signal import (
36
48
  assert_value,
37
49
  observe_value,
38
50
  set_and_wait_for_value,
39
- set_sim_callback,
40
- set_sim_put_proceeds,
41
- set_sim_value,
42
- soft_signal_r_and_backend,
51
+ soft_signal_r_and_setter,
43
52
  soft_signal_rw,
44
53
  wait_for_value,
45
54
  )
46
55
  from .signal_backend import SignalBackend
47
- from .sim_signal_backend import SimSignalBackend
56
+ from .soft_signal_backend import SoftSignalBackend
48
57
  from .standard_readable import ConfigSignal, HintedSignal, StandardReadable
49
58
  from .utils import (
50
59
  DEFAULT_TIMEOUT,
@@ -59,9 +68,15 @@ from .utils import (
59
68
  )
60
69
 
61
70
  __all__ = [
71
+ "assert_mock_put_called_with",
72
+ "callback_on_mock_put",
73
+ "mock_puts_blocked",
74
+ "set_mock_values",
75
+ "reset_mock_put_calls",
62
76
  "SignalBackend",
63
- "SimSignalBackend",
77
+ "SoftSignalBackend",
64
78
  "DetectorControl",
79
+ "MockSignalBackend",
65
80
  "DetectorTrigger",
66
81
  "DetectorWriter",
67
82
  "StandardDetector",
@@ -73,15 +88,15 @@ __all__ = [
73
88
  "SignalW",
74
89
  "SignalRW",
75
90
  "SignalX",
76
- "soft_signal_r_and_backend",
91
+ "soft_signal_r_and_setter",
77
92
  "soft_signal_rw",
78
93
  "observe_value",
79
94
  "set_and_wait_for_value",
80
- "set_sim_callback",
81
- "set_sim_put_proceeds",
82
- "set_sim_value",
95
+ "set_mock_put_proceeds",
96
+ "set_mock_value",
83
97
  "wait_for_value",
84
98
  "AsyncStatus",
99
+ "WatchableAsyncStatus",
85
100
  "DirectoryInfo",
86
101
  "DirectoryProvider",
87
102
  "NameProvider",
@@ -2,28 +2,42 @@
2
2
 
3
3
  import asyncio
4
4
  import functools
5
- from typing import Awaitable, Callable, Coroutine, List, Optional, cast
5
+ import time
6
+ from dataclasses import asdict, replace
7
+ from typing import (
8
+ AsyncIterator,
9
+ Awaitable,
10
+ Callable,
11
+ Generic,
12
+ SupportsFloat,
13
+ Type,
14
+ TypeVar,
15
+ cast,
16
+ )
6
17
 
7
18
  from bluesky.protocols import Status
8
19
 
9
- from .utils import Callback, T
20
+ from ..protocols import Watcher
21
+ from .utils import Callback, P, T, WatcherUpdate
10
22
 
23
+ AS = TypeVar("AS", bound="AsyncStatus")
24
+ WAS = TypeVar("WAS", bound="WatchableAsyncStatus")
11
25
 
12
- class AsyncStatus(Status):
26
+
27
+ class AsyncStatusBase(Status):
13
28
  """Convert asyncio awaitable to bluesky Status interface"""
14
29
 
15
- def __init__(
16
- self,
17
- awaitable: Awaitable,
18
- watchers: Optional[List[Callable]] = None,
19
- ):
30
+ def __init__(self, awaitable: Awaitable, timeout: SupportsFloat | None = None):
31
+ if isinstance(timeout, SupportsFloat):
32
+ timeout = float(timeout)
20
33
  if isinstance(awaitable, asyncio.Task):
21
34
  self.task = awaitable
22
35
  else:
23
- self.task = asyncio.create_task(awaitable) # type: ignore
36
+ self.task = asyncio.create_task(
37
+ asyncio.wait_for(awaitable, timeout=timeout)
38
+ )
24
39
  self.task.add_done_callback(self._run_callbacks)
25
- self._callbacks = cast(List[Callback[Status]], [])
26
- self._watchers = watchers
40
+ self._callbacks: list[Callback[Status]] = []
27
41
 
28
42
  def __await__(self):
29
43
  return self.task.__await__()
@@ -39,15 +53,11 @@ class AsyncStatus(Status):
39
53
  for callback in self._callbacks:
40
54
  callback(self)
41
55
 
42
- # TODO: remove ignore and bump min version when bluesky v1.12.0 is released
43
- def exception( # type: ignore
44
- self, timeout: Optional[float] = 0.0
45
- ) -> Optional[BaseException]:
56
+ def exception(self, timeout: float | None = 0.0) -> BaseException | None:
46
57
  if timeout != 0.0:
47
- raise Exception(
58
+ raise ValueError(
48
59
  "cannot honour any timeout other than 0 in an asynchronous function"
49
60
  )
50
-
51
61
  if self.task.done():
52
62
  try:
53
63
  return self.task.exception()
@@ -67,22 +77,6 @@ class AsyncStatus(Status):
67
77
  and self.task.exception() is None
68
78
  )
69
79
 
70
- def watch(self, watcher: Callable):
71
- """Add watcher to the list of interested parties.
72
-
73
- Arguments as per Bluesky :external+bluesky:meth:`watch` protocol.
74
- """
75
- if self._watchers is not None:
76
- self._watchers.append(watcher)
77
-
78
- @classmethod
79
- def wrap(cls, f: Callable[[T], Coroutine]) -> Callable[[T], "AsyncStatus"]:
80
- @functools.wraps(f)
81
- def wrap_f(self) -> AsyncStatus:
82
- return AsyncStatus(f(self))
83
-
84
- return wrap_f
85
-
86
80
  def __repr__(self) -> str:
87
81
  if self.done:
88
82
  if e := self.exception():
@@ -94,3 +88,72 @@ class AsyncStatus(Status):
94
88
  return f"<{type(self).__name__}, task: {self.task.get_coro()}, {status}>"
95
89
 
96
90
  __str__ = __repr__
91
+
92
+
93
+ class AsyncStatus(AsyncStatusBase):
94
+ @classmethod
95
+ def wrap(cls: Type[AS], f: Callable[P, Awaitable]) -> Callable[P, AS]:
96
+ @functools.wraps(f)
97
+ def wrap_f(*args: P.args, **kwargs: P.kwargs) -> AS:
98
+ # We can't type this more properly because Concatenate/ParamSpec doesn't
99
+ # yet support keywords
100
+ # https://peps.python.org/pep-0612/#concatenating-keyword-parameters
101
+ timeout = kwargs.get("timeout")
102
+ assert isinstance(timeout, SupportsFloat) or timeout is None
103
+ return cls(f(*args, **kwargs), timeout=timeout)
104
+
105
+ # type is actually functools._Wrapped[P, Awaitable, P, AS]
106
+ # but functools._Wrapped is not necessarily available
107
+ return cast(Callable[P, AS], wrap_f)
108
+
109
+
110
+ class WatchableAsyncStatus(AsyncStatusBase, Generic[T]):
111
+ """Convert AsyncIterator of WatcherUpdates to bluesky Status interface."""
112
+
113
+ def __init__(
114
+ self,
115
+ iterator: AsyncIterator[WatcherUpdate[T]],
116
+ timeout: SupportsFloat | None = None,
117
+ ):
118
+ self._watchers: list[Watcher] = []
119
+ self._start = time.monotonic()
120
+ self._last_update: WatcherUpdate[T] | None = None
121
+ super().__init__(self._notify_watchers_from(iterator), timeout)
122
+
123
+ async def _notify_watchers_from(self, iterator: AsyncIterator[WatcherUpdate[T]]):
124
+ async for update in iterator:
125
+ self._last_update = (
126
+ update
127
+ if update.time_elapsed is not None
128
+ else replace(update, time_elapsed=time.monotonic() - self._start)
129
+ )
130
+ for watcher in self._watchers:
131
+ self._update_watcher(watcher, self._last_update)
132
+
133
+ def _update_watcher(self, watcher: Watcher, update: WatcherUpdate[T]):
134
+ vals = asdict(
135
+ update, dict_factory=lambda d: {k: v for k, v in d if v is not None}
136
+ )
137
+ watcher(**vals)
138
+
139
+ def watch(self, watcher: Watcher):
140
+ self._watchers.append(watcher)
141
+ if self._last_update:
142
+ self._update_watcher(watcher, self._last_update)
143
+
144
+ @classmethod
145
+ def wrap(
146
+ cls: Type[WAS],
147
+ f: Callable[P, AsyncIterator[WatcherUpdate[T]]],
148
+ ) -> Callable[P, WAS]:
149
+ """Wrap an AsyncIterator in a WatchableAsyncStatus. If it takes
150
+ 'timeout' as an argument, this must be a float or None, and it
151
+ will be propagated to the status."""
152
+
153
+ @functools.wraps(f)
154
+ def wrap_f(*args: P.args, **kwargs: P.kwargs) -> WAS:
155
+ timeout = kwargs.get("timeout")
156
+ assert isinstance(timeout, SupportsFloat) or timeout is None
157
+ return cls(f(*args, **kwargs), timeout=timeout)
158
+
159
+ return cast(Callable[P, WAS], wrap_f)
@@ -31,9 +31,9 @@ from bluesky.protocols import (
31
31
 
32
32
  from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
33
33
 
34
- from .async_status import AsyncStatus
34
+ from .async_status import AsyncStatus, WatchableAsyncStatus
35
35
  from .device import Device
36
- from .utils import DEFAULT_TIMEOUT, merge_gathered_dicts
36
+ from .utils import DEFAULT_TIMEOUT, WatcherUpdate, merge_gathered_dicts
37
37
 
38
38
  T = TypeVar("T")
39
39
 
@@ -188,7 +188,7 @@ class StandardDetector(
188
188
  self._trigger_info: Optional[TriggerInfo] = None
189
189
  # For kickoff
190
190
  self._watchers: List[Callable] = []
191
- self._fly_status: Optional[AsyncStatus] = None
191
+ self._fly_status: Optional[WatchableAsyncStatus] = None
192
192
  self._fly_start: float
193
193
 
194
194
  self._intial_frame: int
@@ -292,43 +292,37 @@ class StandardDetector(
292
292
  f"Detector {self.controller} needs at least {required}s deadtime, "
293
293
  f"but trigger logic provides only {self._trigger_info.deadtime}s"
294
294
  )
295
-
296
295
  self._arm_status = await self.controller.arm(
297
296
  num=self._trigger_info.num,
298
297
  trigger=self._trigger_info.trigger,
299
298
  exposure=self._trigger_info.livetime,
300
299
  )
301
-
302
- @AsyncStatus.wrap
303
- async def kickoff(self) -> None:
304
- self._fly_status = AsyncStatus(self._fly(), self._watchers)
305
300
  self._fly_start = time.monotonic()
306
301
 
307
- async def _fly(self) -> None:
308
- await self._observe_writer_indicies(self._last_frame)
309
-
310
- async def _observe_writer_indicies(self, end_observation: int):
302
+ @AsyncStatus.wrap
303
+ async def kickoff(self):
304
+ if not self._arm_status:
305
+ raise Exception("Detector not armed!")
306
+
307
+ @WatchableAsyncStatus.wrap
308
+ async def complete(self):
309
+ assert self._arm_status, "Prepare not run"
310
+ assert self._trigger_info
311
311
  async for index in self.writer.observe_indices_written(
312
312
  self._frame_writing_timeout
313
313
  ):
314
- for watcher in self._watchers:
315
- watcher(
316
- name=self.name,
317
- current=index,
318
- initial=self._initial_frame,
319
- target=end_observation,
320
- unit="",
321
- precision=0,
322
- time_elapsed=time.monotonic() - self._fly_start,
323
- )
324
- if index >= end_observation:
314
+ yield WatcherUpdate(
315
+ name=self.name,
316
+ current=index,
317
+ initial=self._initial_frame,
318
+ target=self._trigger_info.num,
319
+ unit="",
320
+ precision=0,
321
+ time_elapsed=time.monotonic() - self._fly_start,
322
+ )
323
+ if index >= self._trigger_info.num:
325
324
  break
326
325
 
327
- @AsyncStatus.wrap
328
- async def complete(self) -> AsyncStatus:
329
- assert self._fly_status, "Kickoff not run"
330
- return await self._fly_status
331
-
332
326
  async def describe_collect(self) -> Dict[str, DataKey]:
333
327
  return self._describe
334
328
 
@@ -2,7 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import sys
7
+ from functools import cached_property
8
+ from logging import LoggerAdapter, getLogger
6
9
  from typing import (
7
10
  Any,
8
11
  Coroutine,
@@ -30,6 +33,9 @@ class Device(HasName):
30
33
  _name: str = ""
31
34
  #: The parent Device if it exists
32
35
  parent: Optional[Device] = None
36
+ # None if connect hasn't started, a Task if it has
37
+ _connect_task: Optional[asyncio.Task] = None
38
+ _connect_mock_arg: bool = False
33
39
 
34
40
  def __init__(self, name: str = "") -> None:
35
41
  self.set_name(name)
@@ -39,6 +45,12 @@ class Device(HasName):
39
45
  """Return the name of the Device"""
40
46
  return self._name
41
47
 
48
+ @cached_property
49
+ def log(self):
50
+ return LoggerAdapter(
51
+ getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
52
+ )
53
+
42
54
  def children(self) -> Iterator[Tuple[str, Device]]:
43
55
  for attr_name, attr in self.__dict__.items():
44
56
  if attr_name != "parent" and isinstance(attr, Device):
@@ -52,30 +64,54 @@ class Device(HasName):
52
64
  name:
53
65
  New name to set
54
66
  """
67
+
68
+ # Ensure self.log is recreated after a name change
69
+ if hasattr(self, "log"):
70
+ del self.log
71
+
55
72
  self._name = name
56
73
  for attr_name, child in self.children():
57
74
  child_name = f"{name}-{attr_name.rstrip('_')}" if name else ""
58
75
  child.set_name(child_name)
59
76
  child.parent = self
60
77
 
61
- async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT):
78
+ async def connect(
79
+ self,
80
+ mock: bool = False,
81
+ timeout: float = DEFAULT_TIMEOUT,
82
+ force_reconnect: bool = False,
83
+ ):
62
84
  """Connect self and all child Devices.
63
85
 
64
86
  Contains a timeout that gets propagated to child.connect methods.
65
87
 
66
88
  Parameters
67
89
  ----------
68
- sim:
69
- If True then connect in simulation mode.
90
+ mock:
91
+ If True then use ``MockSignalBackend`` for all Signals
70
92
  timeout:
71
93
  Time to wait before failing with a TimeoutError.
72
94
  """
73
- coros = {
74
- name: child_device.connect(sim, timeout=timeout)
75
- for name, child_device in self.children()
76
- }
77
- if coros:
78
- await wait_for_connection(**coros)
95
+ # If previous connect with same args has started and not errored, can use it
96
+ can_use_previous_connect = (
97
+ self._connect_task
98
+ and not (self._connect_task.done() and self._connect_task.exception())
99
+ and self._connect_mock_arg == mock
100
+ )
101
+ if force_reconnect or not can_use_previous_connect:
102
+ # Kick off a connection
103
+ coros = {
104
+ name: child_device.connect(
105
+ mock, timeout=timeout, force_reconnect=force_reconnect
106
+ )
107
+ for name, child_device in self.children()
108
+ }
109
+ self._connect_task = asyncio.create_task(wait_for_connection(**coros))
110
+ self._connect_mock_arg = mock
111
+
112
+ assert self._connect_task, "Connect task not created, this shouldn't happen"
113
+ # Wait for it to complete
114
+ await self._connect_task
79
115
 
80
116
 
81
117
  VT = TypeVar("VT", bound=Device)
@@ -105,9 +141,9 @@ class DeviceCollector:
105
141
  If True, call ``device.set_name(variable_name)`` on all collected
106
142
  Devices
107
143
  connect:
108
- If True, call ``device.connect(sim)`` in parallel on all
144
+ If True, call ``device.connect(mock)`` in parallel on all
109
145
  collected Devices
110
- sim:
146
+ mock:
111
147
  If True, connect Signals in simulation mode
112
148
  timeout:
113
149
  How long to wait for connect before logging an exception
@@ -129,12 +165,12 @@ class DeviceCollector:
129
165
  self,
130
166
  set_name=True,
131
167
  connect=True,
132
- sim=False,
168
+ mock=False,
133
169
  timeout: float = 10.0,
134
170
  ):
135
171
  self._set_name = set_name
136
172
  self._connect = connect
137
- self._sim = sim
173
+ self._mock = mock
138
174
  self._timeout = timeout
139
175
  self._names_on_enter: Set[str] = set()
140
176
  self._objects_on_exit: Dict[str, Any] = {}
@@ -168,7 +204,7 @@ class DeviceCollector:
168
204
  obj.set_name(name)
169
205
  if self._connect:
170
206
  connect_coroutines[name] = obj.connect(
171
- self._sim, timeout=self._timeout
207
+ self._mock, timeout=self._timeout
172
208
  )
173
209
 
174
210
  # Connect to all the devices
@@ -0,0 +1,85 @@
1
+ import asyncio
2
+ from functools import cached_property
3
+ from typing import 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):
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 mimick it
35
+
36
+ self.soft_backend = SoftSignalBackend(datatype=datatype)
37
+ else:
38
+ self.soft_backend = 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")
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_descriptor(self, source: str) -> Descriptor:
69
+ return await self.soft_backend.get_descriptor(source)
70
+
71
+ async def get_reading(self) -> Reading:
72
+ return await self.soft_backend.get_reading()
73
+
74
+ async def get_value(self) -> T:
75
+ return await self.soft_backend.get_value()
76
+
77
+ async def get_setpoint(self) -> T:
78
+ """For a soft signal, the setpoint and readback values are the same."""
79
+ return await self.soft_backend.get_setpoint()
80
+
81
+ async def get_datakey(self, source: str) -> Descriptor:
82
+ return await self.soft_backend.get_datakey(source)
83
+
84
+ def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
85
+ self.soft_backend.set_callback(callback)