ophyd-async 0.2.0__py3-none-any.whl → 0.3a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +5 -9
  3. ophyd_async/core/_providers.py +36 -5
  4. ophyd_async/core/async_status.py +3 -3
  5. ophyd_async/core/detector.py +159 -37
  6. ophyd_async/core/device.py +37 -38
  7. ophyd_async/core/device_save_loader.py +96 -23
  8. ophyd_async/core/flyer.py +32 -237
  9. ophyd_async/core/signal.py +11 -4
  10. ophyd_async/core/signal_backend.py +2 -2
  11. ophyd_async/core/sim_signal_backend.py +2 -2
  12. ophyd_async/core/utils.py +75 -29
  13. ophyd_async/epics/_backend/_aioca.py +18 -26
  14. ophyd_async/epics/_backend/_p4p.py +58 -27
  15. ophyd_async/epics/_backend/common.py +20 -0
  16. ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +1 -1
  17. ophyd_async/epics/areadetector/controllers/pilatus_controller.py +1 -1
  18. ophyd_async/epics/areadetector/writers/_hdffile.py +17 -3
  19. ophyd_async/epics/areadetector/writers/hdf_writer.py +21 -15
  20. ophyd_async/epics/pvi.py +70 -0
  21. ophyd_async/epics/signal/__init__.py +0 -2
  22. ophyd_async/panda/__init__.py +5 -2
  23. ophyd_async/panda/panda.py +41 -94
  24. ophyd_async/panda/panda_controller.py +41 -0
  25. ophyd_async/panda/utils.py +15 -0
  26. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/METADATA +2 -2
  27. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/RECORD +31 -28
  28. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/WHEEL +1 -1
  29. ophyd_async/epics/signal/pvi_get.py +0 -22
  30. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/LICENSE +0 -0
  31. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/entry_points.txt +0 -0
  32. {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.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,157 +1,17 @@
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, Optional, 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 Descriptor, Flyable, Preparable, Reading, Stageable
28
5
 
29
6
  from .async_status import AsyncStatus
30
- from .detector import DetectorControl, DetectorTrigger, DetectorWriter
7
+ from .detector import TriggerInfo
31
8
  from .device import Device
32
9
  from .signal import SignalR
33
- from .utils import DEFAULT_TIMEOUT, gather_list, merge_gathered_dicts
10
+ from .utils import merge_gathered_dicts
34
11
 
35
12
  T = TypeVar("T")
36
13
 
37
14
 
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
15
  class TriggerLogic(ABC, Generic[T]):
156
16
  @abstractmethod
157
17
  def trigger_info(self, value: T) -> TriggerInfo:
@@ -172,128 +32,63 @@ class TriggerLogic(ABC, Generic[T]):
172
32
 
173
33
  class HardwareTriggeredFlyable(
174
34
  Device,
175
- Movable,
176
35
  Stageable,
36
+ Preparable,
177
37
  Flyable,
178
- Collectable,
179
- WritesExternalAssets,
180
- HasHints,
181
38
  Generic[T],
182
39
  ):
183
40
  def __init__(
184
41
  self,
185
- detector_group_logic: DetectorGroupLogic,
186
42
  trigger_logic: TriggerLogic[T],
187
43
  configuration_signals: Sequence[SignalR],
188
- trigger_to_frame_timeout: Optional[float] = DEFAULT_TIMEOUT,
189
44
  name: str = "",
190
45
  ):
191
- self._detector_group_logic = detector_group_logic
192
46
  self._trigger_logic = trigger_logic
193
47
  self._configuration_signals = tuple(configuration_signals)
194
48
  self._describe: Dict[str, Descriptor] = {}
195
- self._watchers: List[Callable] = []
196
49
  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
50
+ self._trigger_info: Optional[TriggerInfo] = None
202
51
  super().__init__(name=name)
203
52
 
53
+ @property
54
+ def trigger_logic(self) -> TriggerLogic[T]:
55
+ return self._trigger_logic
56
+
57
+ @property
58
+ def trigger_info(self) -> Optional[TriggerInfo]:
59
+ return self._trigger_info
60
+
204
61
  @AsyncStatus.wrap
205
62
  async def stage(self) -> None:
206
63
  await self.unstage()
207
- self._describe = await self._detector_group_logic.open()
208
- self._offset = 0
209
- self._current_frame = 0
210
-
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))
216
-
217
- async def _set(self, value: T) -> None:
218
- self._offset -= self._current_frame
219
- self._current_frame = 0
220
- await 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
230
-
231
- async def describe_configuration(self) -> Dict[str, Descriptor]:
232
- return await merge_gathered_dicts(
233
- [sig.describe() for sig in self._configuration_signals]
234
- )
65
+ @AsyncStatus.wrap
66
+ async def unstage(self) -> None:
67
+ await self._trigger_logic.stop()
235
68
 
236
- async def read_configuration(self) -> Dict[str, Reading]:
237
- return await merge_gathered_dicts(
238
- [sig.read() for sig in self._configuration_signals]
239
- )
69
+ def prepare(self, value: T) -> AsyncStatus:
70
+ """Setup trajectories"""
71
+ return AsyncStatus(self._prepare(value))
240
72
 
241
- async def describe_collect(self) -> Dict[str, Descriptor]:
242
- return self._describe
73
+ async def _prepare(self, value: T) -> None:
74
+ self._trigger_info = self._trigger_logic.trigger_info(value)
75
+ # Move to start and setup the flyscan
76
+ await self._trigger_logic.prepare(value)
243
77
 
244
78
  @AsyncStatus.wrap
245
79
  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
- )
80
+ self._fly_status = AsyncStatus(self._trigger_logic.start())
284
81
 
285
82
  def complete(self) -> AsyncStatus:
286
83
  assert self._fly_status, "Kickoff not run"
287
84
  return self._fly_status
288
85
 
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(),
86
+ async def describe_configuration(self) -> Dict[str, Descriptor]:
87
+ return await merge_gathered_dicts(
88
+ [sig.describe() for sig in self._configuration_signals]
295
89
  )
296
90
 
297
- @property
298
- def hints(self) -> Hints:
299
- return self._detector_group_logic.hints()
91
+ async def read_configuration(self) -> Dict[str, Reading]:
92
+ return await merge_gathered_dicts(
93
+ [sig.read() for sig in self._configuration_signals]
94
+ )
@@ -58,7 +58,7 @@ class Signal(Device, Generic[T]):
58
58
  def set_name(self, name: str = ""):
59
59
  self._name = name
60
60
 
61
- async def connect(self, sim=False):
61
+ async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT):
62
62
  if sim:
63
63
  self._backend = SimSignalBackend(
64
64
  datatype=self._init_backend.datatype, source=self._init_backend.source
@@ -67,7 +67,7 @@ class Signal(Device, Generic[T]):
67
67
  else:
68
68
  self._backend = self._init_backend
69
69
  _sim_backends.pop(self, None)
70
- await self._backend.connect()
70
+ await self._backend.connect(timeout=timeout)
71
71
 
72
72
  @property
73
73
  def source(self) -> str:
@@ -253,7 +253,7 @@ def set_sim_callback(signal: Signal[T], callback: ReadingValueCallback[T]) -> No
253
253
  return _sim_backends[signal].set_callback(callback)
254
254
 
255
255
 
256
- async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
256
+ async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, None]:
257
257
  """Subscribe to the value of a signal so it can be iterated from.
258
258
 
259
259
  Parameters
@@ -270,10 +270,17 @@ async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
270
270
  do_something_with(value)
271
271
  """
272
272
  q: asyncio.Queue[T] = asyncio.Queue()
273
+ if timeout is None:
274
+ get_value = q.get
275
+ else:
276
+
277
+ async def get_value():
278
+ return await asyncio.wait_for(q.get(), timeout)
279
+
273
280
  signal.subscribe_value(q.put_nowait)
274
281
  try:
275
282
  while True:
276
- yield await q.get()
283
+ yield await get_value()
277
284
  finally:
278
285
  signal.clear_sub(q.put_nowait)
279
286
 
@@ -3,7 +3,7 @@ from typing import Generic, Optional, Type
3
3
 
4
4
  from bluesky.protocols import Descriptor, Reading
5
5
 
6
- from .utils import ReadingValueCallback, T
6
+ from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T
7
7
 
8
8
 
9
9
  class SignalBackend(Generic[T]):
@@ -16,7 +16,7 @@ class SignalBackend(Generic[T]):
16
16
  source: str = ""
17
17
 
18
18
  @abstractmethod
19
- async def connect(self):
19
+ async def connect(self, timeout: float = DEFAULT_TIMEOUT):
20
20
  """Connect to underlying hardware"""
21
21
 
22
22
  @abstractmethod
@@ -12,7 +12,7 @@ from typing import Any, Dict, Generic, Optional, Type, Union, cast, get_origin
12
12
  from bluesky.protocols import Descriptor, Dtype, Reading
13
13
 
14
14
  from .signal_backend import SignalBackend
15
- from .utils import ReadingValueCallback, T, get_dtype
15
+ from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T, get_dtype
16
16
 
17
17
  primitive_dtypes: Dict[type, Dtype] = {
18
18
  str: "string",
@@ -123,7 +123,7 @@ class SimSignalBackend(SignalBackend[T]):
123
123
  self.put_proceeds.set()
124
124
  self.callback: Optional[ReadingValueCallback[T]] = None
125
125
 
126
- async def connect(self) -> None:
126
+ async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
127
127
  self.converter = make_converter(self.datatype)
128
128
  self._initial_value = self.converter.make_initial_value(self.datatype)
129
129
  self._severity = 0