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.
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +5 -9
- ophyd_async/core/_providers.py +36 -5
- ophyd_async/core/async_status.py +3 -3
- ophyd_async/core/detector.py +159 -37
- ophyd_async/core/device.py +37 -38
- ophyd_async/core/device_save_loader.py +96 -23
- ophyd_async/core/flyer.py +32 -237
- ophyd_async/core/signal.py +11 -4
- ophyd_async/core/signal_backend.py +2 -2
- ophyd_async/core/sim_signal_backend.py +2 -2
- ophyd_async/core/utils.py +75 -29
- ophyd_async/epics/_backend/_aioca.py +18 -26
- ophyd_async/epics/_backend/_p4p.py +58 -27
- ophyd_async/epics/_backend/common.py +20 -0
- ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +1 -1
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +1 -1
- ophyd_async/epics/areadetector/writers/_hdffile.py +17 -3
- ophyd_async/epics/areadetector/writers/hdf_writer.py +21 -15
- ophyd_async/epics/pvi.py +70 -0
- ophyd_async/epics/signal/__init__.py +0 -2
- ophyd_async/panda/__init__.py +5 -2
- ophyd_async/panda/panda.py +41 -94
- ophyd_async/panda/panda_controller.py +41 -0
- ophyd_async/panda/utils.py +15 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/METADATA +2 -2
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/RECORD +31 -28
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/WHEEL +1 -1
- ophyd_async/epics/signal/pvi_get.py +0 -22
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/LICENSE +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a1.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
+
ignore : Optional[List[str]]
|
|
59
|
+
Optional list of PVs that should be ignored.
|
|
58
60
|
|
|
59
61
|
Returns
|
|
60
62
|
-------
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
98
|
-
paths in a dictionary. Used as
|
|
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
|
-
|
|
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
|
|
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
|
|
7
|
+
from .detector import TriggerInfo
|
|
31
8
|
from .device import Device
|
|
32
9
|
from .signal import SignalR
|
|
33
|
-
from .utils import
|
|
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.
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
)
|
|
69
|
+
def prepare(self, value: T) -> AsyncStatus:
|
|
70
|
+
"""Setup trajectories"""
|
|
71
|
+
return AsyncStatus(self._prepare(value))
|
|
240
72
|
|
|
241
|
-
async def
|
|
242
|
-
|
|
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.
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
+
)
|
ophyd_async/core/signal.py
CHANGED
|
@@ -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
|
|
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
|