ophyd-async 0.2.0__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.
- ophyd_async/__init__.py +1 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +16 -10
- ophyd_async/core/_providers.py +38 -5
- ophyd_async/core/async_status.py +3 -3
- ophyd_async/core/detector.py +211 -62
- ophyd_async/core/device.py +45 -38
- ophyd_async/core/device_save_loader.py +96 -23
- ophyd_async/core/flyer.py +30 -244
- ophyd_async/core/signal.py +47 -21
- ophyd_async/core/signal_backend.py +7 -4
- ophyd_async/core/sim_signal_backend.py +30 -18
- ophyd_async/core/standard_readable.py +4 -2
- ophyd_async/core/utils.py +93 -30
- ophyd_async/epics/_backend/_aioca.py +30 -36
- ophyd_async/epics/_backend/_p4p.py +75 -41
- ophyd_async/epics/_backend/common.py +25 -0
- ophyd_async/epics/areadetector/__init__.py +4 -0
- ophyd_async/epics/areadetector/aravis.py +69 -0
- ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +1 -1
- ophyd_async/epics/areadetector/controllers/aravis_controller.py +73 -0
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +37 -25
- ophyd_async/epics/areadetector/drivers/aravis_driver.py +154 -0
- ophyd_async/epics/areadetector/drivers/pilatus_driver.py +4 -4
- ophyd_async/epics/areadetector/pilatus.py +50 -0
- ophyd_async/epics/areadetector/writers/_hdffile.py +21 -7
- ophyd_async/epics/areadetector/writers/hdf_writer.py +26 -15
- ophyd_async/epics/demo/__init__.py +33 -3
- ophyd_async/epics/motion/motor.py +20 -14
- ophyd_async/epics/pvi/__init__.py +3 -0
- ophyd_async/epics/pvi/pvi.py +318 -0
- ophyd_async/epics/signal/__init__.py +0 -2
- ophyd_async/epics/signal/signal.py +26 -9
- ophyd_async/panda/__init__.py +19 -5
- ophyd_async/panda/_common_blocks.py +49 -0
- ophyd_async/panda/_hdf_panda.py +48 -0
- ophyd_async/panda/_panda_controller.py +37 -0
- ophyd_async/panda/_trigger.py +39 -0
- ophyd_async/panda/_utils.py +15 -0
- ophyd_async/panda/writers/__init__.py +3 -0
- ophyd_async/panda/writers/_hdf_writer.py +220 -0
- ophyd_async/panda/writers/_panda_hdf_file.py +58 -0
- ophyd_async/planstubs/__init__.py +5 -0
- ophyd_async/planstubs/prepare_trigger_and_dets.py +57 -0
- ophyd_async/protocols.py +73 -0
- ophyd_async/sim/__init__.py +11 -0
- ophyd_async/sim/demo/__init__.py +3 -0
- ophyd_async/sim/demo/sim_motor.py +116 -0
- ophyd_async/sim/pattern_generator.py +318 -0
- ophyd_async/sim/sim_pattern_detector_control.py +55 -0
- ophyd_async/sim/sim_pattern_detector_writer.py +34 -0
- ophyd_async/sim/sim_pattern_generator.py +37 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/METADATA +20 -76
- ophyd_async-0.3a2.dist-info/RECORD +76 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/WHEEL +1 -1
- ophyd_async/epics/signal/pvi_get.py +0 -22
- ophyd_async/panda/panda.py +0 -294
- ophyd_async-0.2.0.dist-info/RECORD +0 -53
- /ophyd_async/panda/{table.py → _table.py} +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/LICENSE +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.2.0.dist-info → ophyd_async-0.3a2.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,170 +1,29 @@
|
|
|
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, 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
|
|
31
7
|
from .device import Device
|
|
32
8
|
from .signal import SignalR
|
|
33
|
-
from .utils import
|
|
9
|
+
from .utils import merge_gathered_dicts
|
|
34
10
|
|
|
35
11
|
T = TypeVar("T")
|
|
36
12
|
|
|
37
13
|
|
|
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
14
|
class TriggerLogic(ABC, Generic[T]):
|
|
156
|
-
@abstractmethod
|
|
157
|
-
def trigger_info(self, value: T) -> TriggerInfo:
|
|
158
|
-
"""Return info about triggers that will be produced for a given value"""
|
|
159
|
-
|
|
160
15
|
@abstractmethod
|
|
161
16
|
async def prepare(self, value: T):
|
|
162
17
|
"""Move to the start of the flyscan"""
|
|
163
18
|
|
|
164
19
|
@abstractmethod
|
|
165
|
-
async def
|
|
20
|
+
async def kickoff(self):
|
|
166
21
|
"""Start the flyscan"""
|
|
167
22
|
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def complete(self):
|
|
25
|
+
"""Block until the flyscan is done"""
|
|
26
|
+
|
|
168
27
|
@abstractmethod
|
|
169
28
|
async def stop(self):
|
|
170
29
|
"""Stop flying and wait everything to be stopped"""
|
|
@@ -172,61 +31,48 @@ class TriggerLogic(ABC, Generic[T]):
|
|
|
172
31
|
|
|
173
32
|
class HardwareTriggeredFlyable(
|
|
174
33
|
Device,
|
|
175
|
-
Movable,
|
|
176
34
|
Stageable,
|
|
35
|
+
Preparable,
|
|
177
36
|
Flyable,
|
|
178
|
-
Collectable,
|
|
179
|
-
WritesExternalAssets,
|
|
180
|
-
HasHints,
|
|
181
37
|
Generic[T],
|
|
182
38
|
):
|
|
183
39
|
def __init__(
|
|
184
40
|
self,
|
|
185
|
-
detector_group_logic: DetectorGroupLogic,
|
|
186
41
|
trigger_logic: TriggerLogic[T],
|
|
187
42
|
configuration_signals: Sequence[SignalR],
|
|
188
|
-
trigger_to_frame_timeout: Optional[float] = DEFAULT_TIMEOUT,
|
|
189
43
|
name: str = "",
|
|
190
44
|
):
|
|
191
|
-
self._detector_group_logic = detector_group_logic
|
|
192
45
|
self._trigger_logic = trigger_logic
|
|
193
46
|
self._configuration_signals = tuple(configuration_signals)
|
|
194
|
-
self._describe: Dict[str, Descriptor] = {}
|
|
195
|
-
self._watchers: List[Callable] = []
|
|
196
|
-
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
|
|
202
47
|
super().__init__(name=name)
|
|
203
48
|
|
|
49
|
+
@property
|
|
50
|
+
def trigger_logic(self) -> TriggerLogic[T]:
|
|
51
|
+
return self._trigger_logic
|
|
52
|
+
|
|
204
53
|
@AsyncStatus.wrap
|
|
205
54
|
async def stage(self) -> None:
|
|
206
55
|
await self.unstage()
|
|
207
|
-
self._describe = await self._detector_group_logic.open()
|
|
208
|
-
self._offset = 0
|
|
209
|
-
self._current_frame = 0
|
|
210
56
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
# so subtract current_frame from both sides
|
|
215
|
-
return AsyncStatus(self._set(value))
|
|
57
|
+
@AsyncStatus.wrap
|
|
58
|
+
async def unstage(self) -> None:
|
|
59
|
+
await self._trigger_logic.stop()
|
|
216
60
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
self.
|
|
220
|
-
await self._prepare(value)
|
|
61
|
+
def prepare(self, value: T) -> AsyncStatus:
|
|
62
|
+
"""Setup trajectories"""
|
|
63
|
+
return AsyncStatus(self._prepare(value))
|
|
221
64
|
|
|
222
|
-
async def _prepare(self, value: T):
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
)
|
|
229
|
-
|
|
65
|
+
async def _prepare(self, value: T) -> None:
|
|
66
|
+
# Move to start and setup the flyscan
|
|
67
|
+
await self._trigger_logic.prepare(value)
|
|
68
|
+
|
|
69
|
+
@AsyncStatus.wrap
|
|
70
|
+
async def kickoff(self) -> None:
|
|
71
|
+
await self._trigger_logic.kickoff()
|
|
72
|
+
|
|
73
|
+
@AsyncStatus.wrap
|
|
74
|
+
async def complete(self) -> None:
|
|
75
|
+
await self._trigger_logic.complete()
|
|
230
76
|
|
|
231
77
|
async def describe_configuration(self) -> Dict[str, Descriptor]:
|
|
232
78
|
return await merge_gathered_dicts(
|
|
@@ -237,63 +83,3 @@ class HardwareTriggeredFlyable(
|
|
|
237
83
|
return await merge_gathered_dicts(
|
|
238
84
|
[sig.read() for sig in self._configuration_signals]
|
|
239
85
|
)
|
|
240
|
-
|
|
241
|
-
async def describe_collect(self) -> Dict[str, Descriptor]:
|
|
242
|
-
return self._describe
|
|
243
|
-
|
|
244
|
-
@AsyncStatus.wrap
|
|
245
|
-
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
|
-
)
|
|
284
|
-
|
|
285
|
-
def complete(self) -> AsyncStatus:
|
|
286
|
-
assert self._fly_status, "Kickoff not run"
|
|
287
|
-
return self._fly_status
|
|
288
|
-
|
|
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(),
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
@property
|
|
298
|
-
def hints(self) -> Hints:
|
|
299
|
-
return self._detector_group_logic.hints()
|
ophyd_async/core/signal.py
CHANGED
|
@@ -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,34 +46,28 @@ 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,
|
|
49
|
+
self,
|
|
50
|
+
backend: SignalBackend[T],
|
|
51
|
+
timeout: Optional[float] = DEFAULT_TIMEOUT,
|
|
52
|
+
name: str = "",
|
|
49
53
|
) -> None:
|
|
50
|
-
|
|
54
|
+
super().__init__(name)
|
|
51
55
|
self._timeout = timeout
|
|
52
56
|
self._init_backend = self._backend = backend
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
def name(self) -> str:
|
|
56
|
-
return self._name
|
|
57
|
-
|
|
58
|
-
def set_name(self, name: str = ""):
|
|
59
|
-
self._name = name
|
|
60
|
-
|
|
61
|
-
async def connect(self, sim=False):
|
|
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
|
|
69
64
|
_sim_backends.pop(self, None)
|
|
70
|
-
await self._backend.connect()
|
|
65
|
+
await self._backend.connect(timeout=timeout)
|
|
71
66
|
|
|
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],
|
|
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,7 +248,31 @@ def set_sim_callback(signal: Signal[T], callback: ReadingValueCallback[T]) -> No
|
|
|
253
248
|
return _sim_backends[signal].set_callback(callback)
|
|
254
249
|
|
|
255
250
|
|
|
256
|
-
|
|
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
|
+
|
|
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
|
|
|
259
278
|
Parameters
|
|
@@ -270,10 +289,17 @@ async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
|
|
|
270
289
|
do_something_with(value)
|
|
271
290
|
"""
|
|
272
291
|
q: asyncio.Queue[T] = asyncio.Queue()
|
|
292
|
+
if timeout is None:
|
|
293
|
+
get_value = q.get
|
|
294
|
+
else:
|
|
295
|
+
|
|
296
|
+
async def get_value():
|
|
297
|
+
return await asyncio.wait_for(q.get(), timeout)
|
|
298
|
+
|
|
273
299
|
signal.subscribe_value(q.put_nowait)
|
|
274
300
|
try:
|
|
275
301
|
while True:
|
|
276
|
-
yield await
|
|
302
|
+
yield await get_value()
|
|
277
303
|
finally:
|
|
278
304
|
signal.clear_sub(q.put_nowait)
|
|
279
305
|
|
|
@@ -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]):
|
|
@@ -13,10 +13,13 @@ class SignalBackend(Generic[T]):
|
|
|
13
13
|
datatype: Optional[Type[T]] = None
|
|
14
14
|
|
|
15
15
|
#: Like ca://PV_PREFIX:SIGNAL
|
|
16
|
-
|
|
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
|
-
async def connect(self):
|
|
22
|
+
async def connect(self, timeout: float = DEFAULT_TIMEOUT):
|
|
20
23
|
"""Connect to underlying hardware"""
|
|
21
24
|
|
|
22
25
|
@abstractmethod
|
|
@@ -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
|