ophyd-async 0.1.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 +47 -12
- ophyd_async/core/_providers.py +66 -0
- ophyd_async/core/async_status.py +7 -5
- ophyd_async/core/detector.py +321 -0
- ophyd_async/core/device.py +184 -0
- ophyd_async/core/device_save_loader.py +286 -0
- ophyd_async/core/flyer.py +94 -0
- ophyd_async/core/{_device/_signal/signal.py → signal.py} +46 -18
- ophyd_async/core/{_device/_backend/signal_backend.py → signal_backend.py} +6 -2
- ophyd_async/core/{_device/_backend/sim_signal_backend.py → sim_signal_backend.py} +6 -2
- ophyd_async/core/{_device/standard_readable.py → standard_readable.py} +3 -3
- ophyd_async/core/utils.py +79 -29
- ophyd_async/epics/_backend/_aioca.py +38 -25
- ophyd_async/epics/_backend/_p4p.py +62 -27
- ophyd_async/epics/_backend/common.py +20 -0
- ophyd_async/epics/areadetector/__init__.py +10 -13
- ophyd_async/epics/areadetector/controllers/__init__.py +4 -0
- ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +52 -0
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +49 -0
- ophyd_async/epics/areadetector/drivers/__init__.py +15 -0
- ophyd_async/epics/areadetector/drivers/ad_base.py +111 -0
- ophyd_async/epics/areadetector/drivers/pilatus_driver.py +18 -0
- ophyd_async/epics/areadetector/single_trigger_det.py +4 -4
- ophyd_async/epics/areadetector/utils.py +91 -3
- ophyd_async/epics/areadetector/writers/__init__.py +5 -0
- ophyd_async/epics/areadetector/writers/_hdfdataset.py +10 -0
- ophyd_async/epics/areadetector/writers/_hdffile.py +54 -0
- ophyd_async/epics/areadetector/writers/hdf_writer.py +133 -0
- ophyd_async/epics/areadetector/{nd_file_hdf.py → writers/nd_file_hdf.py} +22 -5
- ophyd_async/epics/areadetector/writers/nd_plugin.py +30 -0
- ophyd_async/epics/demo/__init__.py +3 -2
- ophyd_async/epics/demo/demo_ad_sim_detector.py +35 -0
- ophyd_async/epics/motion/motor.py +2 -1
- ophyd_async/epics/pvi.py +70 -0
- ophyd_async/epics/signal/__init__.py +0 -2
- ophyd_async/epics/signal/signal.py +1 -1
- ophyd_async/panda/__init__.py +12 -8
- ophyd_async/panda/panda.py +43 -134
- ophyd_async/panda/panda_controller.py +41 -0
- ophyd_async/panda/table.py +158 -0
- ophyd_async/panda/utils.py +15 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/METADATA +49 -42
- ophyd_async-0.3a1.dist-info/RECORD +56 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device/__init__.py +0 -0
- ophyd_async/core/_device/_backend/__init__.py +0 -0
- ophyd_async/core/_device/_signal/__init__.py +0 -0
- ophyd_async/core/_device/device.py +0 -60
- ophyd_async/core/_device/device_collector.py +0 -121
- ophyd_async/core/_device/device_vector.py +0 -14
- ophyd_async/epics/areadetector/ad_driver.py +0 -18
- ophyd_async/epics/areadetector/directory_provider.py +0 -18
- ophyd_async/epics/areadetector/hdf_streamer_det.py +0 -167
- ophyd_async/epics/areadetector/nd_plugin.py +0 -13
- ophyd_async/epics/signal/pvi_get.py +0 -22
- ophyd_async-0.1.0.dist-info/RECORD +0 -45
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/LICENSE +0 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.1.0.dist-info → ophyd_async-0.3a1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from functools import partial
|
|
3
|
+
from typing import Any, Callable, Dict, Generator, List, Optional, Sequence, Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import numpy.typing as npt
|
|
7
|
+
import yaml
|
|
8
|
+
from bluesky.plan_stubs import abs_set, wait
|
|
9
|
+
from bluesky.protocols import Location
|
|
10
|
+
from bluesky.utils import Msg
|
|
11
|
+
from epicscorelibs.ca.dbr import ca_array, ca_float, ca_int, ca_str
|
|
12
|
+
|
|
13
|
+
from .device import Device
|
|
14
|
+
from .signal import SignalRW
|
|
15
|
+
|
|
16
|
+
CaType = Union[ca_float, ca_int, ca_str, ca_array]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ndarray_representer(dumper: yaml.Dumper, array: npt.NDArray[Any]) -> yaml.Node:
|
|
20
|
+
return dumper.represent_sequence(
|
|
21
|
+
"tag:yaml.org,2002:seq", array.tolist(), flow_style=True
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def ca_dbr_representer(dumper: yaml.Dumper, value: CaType) -> yaml.Node:
|
|
26
|
+
# if it's an array, just call ndarray_representer...
|
|
27
|
+
represent_array = partial(ndarray_representer, dumper)
|
|
28
|
+
|
|
29
|
+
representers: Dict[CaType, Callable[[CaType], yaml.Node]] = {
|
|
30
|
+
ca_float: dumper.represent_float,
|
|
31
|
+
ca_int: dumper.represent_int,
|
|
32
|
+
ca_str: dumper.represent_str,
|
|
33
|
+
ca_array: represent_array,
|
|
34
|
+
}
|
|
35
|
+
return representers[type(value)](value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OphydDumper(yaml.Dumper):
|
|
39
|
+
def represent_data(self, data: Any) -> Any:
|
|
40
|
+
if isinstance(data, Enum):
|
|
41
|
+
return self.represent_data(data.value)
|
|
42
|
+
return super(OphydDumper, self).represent_data(data)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_signal_values(
|
|
46
|
+
signals: Dict[str, SignalRW[Any]], ignore: Optional[List[str]] = None
|
|
47
|
+
) -> Generator[Msg, Sequence[Location[Any]], Dict[str, Any]]:
|
|
48
|
+
"""Get signal values in bulk.
|
|
49
|
+
|
|
50
|
+
Used as part of saving the signals of a device to a yaml file.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
signals : Dict[str, SignalRW]
|
|
55
|
+
Dictionary with pv names and matching SignalRW values. Often the direct result
|
|
56
|
+
of :func:`walk_rw_signals`.
|
|
57
|
+
|
|
58
|
+
ignore : Optional[List[str]]
|
|
59
|
+
Optional list of PVs that should be ignored.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
Dict[str, Any]
|
|
64
|
+
A dictionary containing pv names and their associated values. Ignored pvs are
|
|
65
|
+
set to None.
|
|
66
|
+
|
|
67
|
+
See Also
|
|
68
|
+
--------
|
|
69
|
+
:func:`ophyd_async.core.walk_rw_signals`
|
|
70
|
+
:func:`ophyd_async.core.save_to_yaml`
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
ignore = ignore or []
|
|
74
|
+
selected_signals = {
|
|
75
|
+
key: signal for key, signal in signals.items() if key not in ignore
|
|
76
|
+
}
|
|
77
|
+
selected_values = yield Msg("locate", *selected_signals.values())
|
|
78
|
+
|
|
79
|
+
# TODO: investigate wrong type hints
|
|
80
|
+
if isinstance(selected_values, dict):
|
|
81
|
+
selected_values = [selected_values] # type: ignore
|
|
82
|
+
|
|
83
|
+
assert selected_values is not None, "No signalRW's were able to be located"
|
|
84
|
+
named_values = {
|
|
85
|
+
key: value["setpoint"] for key, value in zip(selected_signals, selected_values)
|
|
86
|
+
}
|
|
87
|
+
# Ignored values place in with value None so we know which ones were ignored
|
|
88
|
+
named_values.update({key: None for key in ignore})
|
|
89
|
+
return named_values
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def walk_rw_signals(
|
|
93
|
+
device: Device, path_prefix: Optional[str] = ""
|
|
94
|
+
) -> Dict[str, SignalRW[Any]]:
|
|
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
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
device : Device
|
|
103
|
+
Ophyd device to retrieve read-write signals from.
|
|
104
|
+
|
|
105
|
+
path_prefix : str
|
|
106
|
+
For internal use, leave blank when calling the method.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
SignalRWs : dict
|
|
111
|
+
A dictionary matching the string attribute path of a SignalRW with the
|
|
112
|
+
signal itself.
|
|
113
|
+
|
|
114
|
+
See Also
|
|
115
|
+
--------
|
|
116
|
+
:func:`ophyd_async.core.get_signal_values`
|
|
117
|
+
:func:`ophyd_async.core.save_to_yaml`
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
if not path_prefix:
|
|
122
|
+
path_prefix = ""
|
|
123
|
+
|
|
124
|
+
signals: Dict[str, SignalRW[Any]] = {}
|
|
125
|
+
for attr_name, attr in device.children():
|
|
126
|
+
dot_path = f"{path_prefix}{attr_name}"
|
|
127
|
+
if type(attr) is SignalRW:
|
|
128
|
+
signals[dot_path] = attr
|
|
129
|
+
attr_signals = walk_rw_signals(attr, path_prefix=dot_path + ".")
|
|
130
|
+
signals.update(attr_signals)
|
|
131
|
+
return signals
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def save_to_yaml(phases: Sequence[Dict[str, Any]], save_path: str) -> None:
|
|
135
|
+
"""Plan which serialises a phase or set of phases of SignalRWs to a yaml file.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
phases : dict or list of dicts
|
|
140
|
+
The values to save. Each item in the list is a seperate phase used when loading
|
|
141
|
+
a device. In general this variable be the return value of `get_signal_values`.
|
|
142
|
+
|
|
143
|
+
save_path : str
|
|
144
|
+
Path of the yaml file to write to
|
|
145
|
+
|
|
146
|
+
See Also
|
|
147
|
+
--------
|
|
148
|
+
:func:`ophyd_async.core.walk_rw_signals`
|
|
149
|
+
:func:`ophyd_async.core.get_signal_values`
|
|
150
|
+
:func:`ophyd_async.core.load_from_yaml`
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
yaml.add_representer(np.ndarray, ndarray_representer, Dumper=yaml.Dumper)
|
|
154
|
+
|
|
155
|
+
yaml.add_representer(ca_float, ca_dbr_representer, Dumper=yaml.Dumper)
|
|
156
|
+
yaml.add_representer(ca_int, ca_dbr_representer, Dumper=yaml.Dumper)
|
|
157
|
+
yaml.add_representer(ca_str, ca_dbr_representer, Dumper=yaml.Dumper)
|
|
158
|
+
yaml.add_representer(ca_array, ca_dbr_representer, Dumper=yaml.Dumper)
|
|
159
|
+
|
|
160
|
+
with open(save_path, "w") as file:
|
|
161
|
+
yaml.dump(phases, file, Dumper=OphydDumper, default_flow_style=False)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def load_from_yaml(save_path: str) -> Sequence[Dict[str, Any]]:
|
|
165
|
+
"""Plan that returns a list of dicts with saved signal values from a yaml file.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
save_path : str
|
|
170
|
+
Path of the yaml file to load from
|
|
171
|
+
|
|
172
|
+
See Also
|
|
173
|
+
--------
|
|
174
|
+
:func:`ophyd_async.core.save_to_yaml`
|
|
175
|
+
:func:`ophyd_async.core.set_signal_values`
|
|
176
|
+
"""
|
|
177
|
+
with open(save_path, "r") as file:
|
|
178
|
+
return yaml.full_load(file)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def set_signal_values(
|
|
182
|
+
signals: Dict[str, SignalRW[Any]], values: Sequence[Dict[str, Any]]
|
|
183
|
+
) -> Generator[Msg, None, None]:
|
|
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
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
signals : Dict[str, SignalRW[Any]]
|
|
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.
|
|
194
|
+
|
|
195
|
+
values : Sequence[Dict[str, Any]]
|
|
196
|
+
List of dictionaries of signal name and value pairs, if a signal matches
|
|
197
|
+
the name of one in the signals argument, sets the signal to that value.
|
|
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.
|
|
200
|
+
|
|
201
|
+
See Also
|
|
202
|
+
--------
|
|
203
|
+
:func:`ophyd_async.core.load_from_yaml`
|
|
204
|
+
:func:`ophyd_async.core.walk_rw_signals`
|
|
205
|
+
"""
|
|
206
|
+
# For each phase, set all the signals,
|
|
207
|
+
# load them to the correct value and wait for the load to complete
|
|
208
|
+
for phase_number, phase in enumerate(values):
|
|
209
|
+
# Key is signal name
|
|
210
|
+
for key, value in phase.items():
|
|
211
|
+
# Skip ignored values
|
|
212
|
+
if value is None:
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
if key in signals:
|
|
216
|
+
yield from abs_set(
|
|
217
|
+
signals[key], value, group=f"load-phase{phase_number}"
|
|
218
|
+
)
|
|
219
|
+
|
|
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)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Dict, Generic, Optional, Sequence, TypeVar
|
|
3
|
+
|
|
4
|
+
from bluesky.protocols import Descriptor, Flyable, Preparable, Reading, Stageable
|
|
5
|
+
|
|
6
|
+
from .async_status import AsyncStatus
|
|
7
|
+
from .detector import TriggerInfo
|
|
8
|
+
from .device import Device
|
|
9
|
+
from .signal import SignalR
|
|
10
|
+
from .utils import merge_gathered_dicts
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TriggerLogic(ABC, Generic[T]):
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def trigger_info(self, value: T) -> TriggerInfo:
|
|
18
|
+
"""Return info about triggers that will be produced for a given value"""
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
async def prepare(self, value: T):
|
|
22
|
+
"""Move to the start of the flyscan"""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
async def start(self):
|
|
26
|
+
"""Start the flyscan"""
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def stop(self):
|
|
30
|
+
"""Stop flying and wait everything to be stopped"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HardwareTriggeredFlyable(
|
|
34
|
+
Device,
|
|
35
|
+
Stageable,
|
|
36
|
+
Preparable,
|
|
37
|
+
Flyable,
|
|
38
|
+
Generic[T],
|
|
39
|
+
):
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
trigger_logic: TriggerLogic[T],
|
|
43
|
+
configuration_signals: Sequence[SignalR],
|
|
44
|
+
name: str = "",
|
|
45
|
+
):
|
|
46
|
+
self._trigger_logic = trigger_logic
|
|
47
|
+
self._configuration_signals = tuple(configuration_signals)
|
|
48
|
+
self._describe: Dict[str, Descriptor] = {}
|
|
49
|
+
self._fly_status: Optional[AsyncStatus] = None
|
|
50
|
+
self._trigger_info: Optional[TriggerInfo] = None
|
|
51
|
+
super().__init__(name=name)
|
|
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
|
+
|
|
61
|
+
@AsyncStatus.wrap
|
|
62
|
+
async def stage(self) -> None:
|
|
63
|
+
await self.unstage()
|
|
64
|
+
|
|
65
|
+
@AsyncStatus.wrap
|
|
66
|
+
async def unstage(self) -> None:
|
|
67
|
+
await self._trigger_logic.stop()
|
|
68
|
+
|
|
69
|
+
def prepare(self, value: T) -> AsyncStatus:
|
|
70
|
+
"""Setup trajectories"""
|
|
71
|
+
return AsyncStatus(self._prepare(value))
|
|
72
|
+
|
|
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)
|
|
77
|
+
|
|
78
|
+
@AsyncStatus.wrap
|
|
79
|
+
async def kickoff(self) -> None:
|
|
80
|
+
self._fly_status = AsyncStatus(self._trigger_logic.start())
|
|
81
|
+
|
|
82
|
+
def complete(self) -> AsyncStatus:
|
|
83
|
+
assert self._fly_status, "Kickoff not run"
|
|
84
|
+
return self._fly_status
|
|
85
|
+
|
|
86
|
+
async def describe_configuration(self) -> Dict[str, Descriptor]:
|
|
87
|
+
return await merge_gathered_dicts(
|
|
88
|
+
[sig.describe() for sig in self._configuration_signals]
|
|
89
|
+
)
|
|
90
|
+
|
|
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
|
+
)
|
|
@@ -6,6 +6,8 @@ from typing import AsyncGenerator, Callable, Dict, Generic, Optional, Union
|
|
|
6
6
|
|
|
7
7
|
from bluesky.protocols import (
|
|
8
8
|
Descriptor,
|
|
9
|
+
Locatable,
|
|
10
|
+
Location,
|
|
9
11
|
Movable,
|
|
10
12
|
Readable,
|
|
11
13
|
Reading,
|
|
@@ -13,11 +15,11 @@ from bluesky.protocols import (
|
|
|
13
15
|
Subscribable,
|
|
14
16
|
)
|
|
15
17
|
|
|
16
|
-
from
|
|
17
|
-
from
|
|
18
|
-
from
|
|
19
|
-
from
|
|
20
|
-
from
|
|
18
|
+
from .async_status import AsyncStatus
|
|
19
|
+
from .device import Device
|
|
20
|
+
from .signal_backend import SignalBackend
|
|
21
|
+
from .sim_signal_backend import SimSignalBackend
|
|
22
|
+
from .utils import DEFAULT_TIMEOUT, Callback, ReadingValueCallback, T
|
|
21
23
|
|
|
22
24
|
_sim_backends: Dict[Signal, SimSignalBackend] = {}
|
|
23
25
|
|
|
@@ -56,7 +58,7 @@ class Signal(Device, Generic[T]):
|
|
|
56
58
|
def set_name(self, name: str = ""):
|
|
57
59
|
self._name = name
|
|
58
60
|
|
|
59
|
-
async def connect(self, sim=False):
|
|
61
|
+
async def connect(self, sim=False, timeout=DEFAULT_TIMEOUT):
|
|
60
62
|
if sim:
|
|
61
63
|
self._backend = SimSignalBackend(
|
|
62
64
|
datatype=self._init_backend.datatype, source=self._init_backend.source
|
|
@@ -65,7 +67,7 @@ class Signal(Device, Generic[T]):
|
|
|
65
67
|
else:
|
|
66
68
|
self._backend = self._init_backend
|
|
67
69
|
_sim_backends.pop(self, None)
|
|
68
|
-
await self._backend.connect()
|
|
70
|
+
await self._backend.connect(timeout=timeout)
|
|
69
71
|
|
|
70
72
|
@property
|
|
71
73
|
def source(self) -> str:
|
|
@@ -196,25 +198,40 @@ class SignalR(Signal[T], Readable, Stageable, Subscribable):
|
|
|
196
198
|
self._del_cache(self._get_cache().set_staged(False))
|
|
197
199
|
|
|
198
200
|
|
|
201
|
+
USE_DEFAULT_TIMEOUT = "USE_DEFAULT_TIMEOUT"
|
|
202
|
+
|
|
203
|
+
|
|
199
204
|
class SignalW(Signal[T], Movable):
|
|
200
205
|
"""Signal that can be set"""
|
|
201
206
|
|
|
202
|
-
def set(self, value: T, wait=True, timeout=
|
|
207
|
+
def set(self, value: T, wait=True, timeout=USE_DEFAULT_TIMEOUT) -> AsyncStatus:
|
|
203
208
|
"""Set the value and return a status saying when it's done"""
|
|
204
|
-
|
|
209
|
+
if timeout is USE_DEFAULT_TIMEOUT:
|
|
210
|
+
timeout = self._timeout
|
|
211
|
+
coro = self._backend.put(value, wait=wait, timeout=timeout)
|
|
205
212
|
return AsyncStatus(coro)
|
|
206
213
|
|
|
207
214
|
|
|
208
|
-
class SignalRW(SignalR[T], SignalW[T]):
|
|
215
|
+
class SignalRW(SignalR[T], SignalW[T], Locatable):
|
|
209
216
|
"""Signal that can be both read and set"""
|
|
210
217
|
|
|
218
|
+
async def locate(self) -> Location:
|
|
219
|
+
location: Location = {
|
|
220
|
+
"setpoint": await self._backend.get_setpoint(),
|
|
221
|
+
"readback": await self.get_value(),
|
|
222
|
+
}
|
|
223
|
+
return location
|
|
224
|
+
|
|
211
225
|
|
|
212
226
|
class SignalX(Signal):
|
|
213
227
|
"""Signal that puts the default value"""
|
|
214
228
|
|
|
215
|
-
|
|
216
|
-
"""
|
|
217
|
-
|
|
229
|
+
def trigger(self, wait=True, timeout=USE_DEFAULT_TIMEOUT) -> AsyncStatus:
|
|
230
|
+
"""Trigger the action and return a status saying when it's done"""
|
|
231
|
+
if timeout is USE_DEFAULT_TIMEOUT:
|
|
232
|
+
timeout = self._timeout
|
|
233
|
+
coro = self._backend.put(None, wait=wait, timeout=timeout)
|
|
234
|
+
return AsyncStatus(coro)
|
|
218
235
|
|
|
219
236
|
|
|
220
237
|
def set_sim_value(signal: Signal[T], value: T):
|
|
@@ -236,7 +253,7 @@ def set_sim_callback(signal: Signal[T], callback: ReadingValueCallback[T]) -> No
|
|
|
236
253
|
return _sim_backends[signal].set_callback(callback)
|
|
237
254
|
|
|
238
255
|
|
|
239
|
-
async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
|
|
256
|
+
async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, None]:
|
|
240
257
|
"""Subscribe to the value of a signal so it can be iterated from.
|
|
241
258
|
|
|
242
259
|
Parameters
|
|
@@ -253,17 +270,24 @@ async def observe_value(signal: SignalR[T]) -> AsyncGenerator[T, None]:
|
|
|
253
270
|
do_something_with(value)
|
|
254
271
|
"""
|
|
255
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
|
+
|
|
256
280
|
signal.subscribe_value(q.put_nowait)
|
|
257
281
|
try:
|
|
258
282
|
while True:
|
|
259
|
-
yield await
|
|
283
|
+
yield await get_value()
|
|
260
284
|
finally:
|
|
261
285
|
signal.clear_sub(q.put_nowait)
|
|
262
286
|
|
|
263
287
|
|
|
264
288
|
class _ValueChecker(Generic[T]):
|
|
265
289
|
def __init__(self, matcher: Callable[[T], bool], matcher_name: str):
|
|
266
|
-
self._last_value: Optional[T]
|
|
290
|
+
self._last_value: Optional[T] = None
|
|
267
291
|
self._matcher = matcher
|
|
268
292
|
self._matcher_name = matcher_name
|
|
269
293
|
|
|
@@ -273,7 +297,7 @@ class _ValueChecker(Generic[T]):
|
|
|
273
297
|
if self._matcher(value):
|
|
274
298
|
return
|
|
275
299
|
|
|
276
|
-
async def wait_for_value(self, signal: SignalR[T], timeout: float):
|
|
300
|
+
async def wait_for_value(self, signal: SignalR[T], timeout: Optional[float]):
|
|
277
301
|
try:
|
|
278
302
|
await asyncio.wait_for(self._wait_for_value(signal), timeout)
|
|
279
303
|
except asyncio.TimeoutError as e:
|
|
@@ -284,7 +308,7 @@ class _ValueChecker(Generic[T]):
|
|
|
284
308
|
|
|
285
309
|
|
|
286
310
|
async def wait_for_value(
|
|
287
|
-
signal: SignalR[T], match: Union[T, Callable[[T], bool]], timeout: float
|
|
311
|
+
signal: SignalR[T], match: Union[T, Callable[[T], bool]], timeout: Optional[float]
|
|
288
312
|
):
|
|
289
313
|
"""Wait for a signal to have a matching value.
|
|
290
314
|
|
|
@@ -330,6 +354,10 @@ async def set_and_wait_for_value(
|
|
|
330
354
|
- Read the same Signal to check the operation has started
|
|
331
355
|
- Return the Status so calling code can wait for operation to complete
|
|
332
356
|
|
|
357
|
+
This function sets a signal to a specified value, optionally with or without a
|
|
358
|
+
ca/pv put callback, and waits for the readback value of the signal to match the
|
|
359
|
+
value it was set to.
|
|
360
|
+
|
|
333
361
|
Parameters
|
|
334
362
|
----------
|
|
335
363
|
signal:
|
|
@@ -3,7 +3,7 @@ from typing import Generic, Optional, Type
|
|
|
3
3
|
|
|
4
4
|
from bluesky.protocols import Descriptor, Reading
|
|
5
5
|
|
|
6
|
-
from
|
|
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
|
|
@@ -35,6 +35,10 @@ class SignalBackend(Generic[T]):
|
|
|
35
35
|
async def get_value(self) -> T:
|
|
36
36
|
"""The current value"""
|
|
37
37
|
|
|
38
|
+
@abstractmethod
|
|
39
|
+
async def get_setpoint(self) -> T:
|
|
40
|
+
"""The point that a signal was requested to move to."""
|
|
41
|
+
|
|
38
42
|
@abstractmethod
|
|
39
43
|
def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
|
|
40
44
|
"""Observe changes to the current value, timestamp and severity"""
|
|
@@ -11,8 +11,8 @@ from typing import Any, Dict, Generic, Optional, Type, Union, cast, get_origin
|
|
|
11
11
|
|
|
12
12
|
from bluesky.protocols import Descriptor, Dtype, Reading
|
|
13
13
|
|
|
14
|
-
from ...utils import ReadingValueCallback, T, get_dtype
|
|
15
14
|
from .signal_backend import SignalBackend
|
|
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
|
|
@@ -161,6 +161,10 @@ class SimSignalBackend(SignalBackend[T]):
|
|
|
161
161
|
async def get_value(self) -> T:
|
|
162
162
|
return self.converter.value(self._value)
|
|
163
163
|
|
|
164
|
+
async def get_setpoint(self) -> T:
|
|
165
|
+
"""For a simulated backend, the setpoint and readback values are the same."""
|
|
166
|
+
return await self.get_value()
|
|
167
|
+
|
|
164
168
|
def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
|
|
165
169
|
if callback:
|
|
166
170
|
assert not self.callback, "Cannot set a callback when one is already set"
|
|
@@ -2,10 +2,10 @@ from typing import Dict, Sequence, Tuple
|
|
|
2
2
|
|
|
3
3
|
from bluesky.protocols import Configurable, Descriptor, Readable, Reading, Stageable
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from ..utils import merge_gathered_dicts
|
|
7
|
-
from ._signal.signal import SignalR
|
|
5
|
+
from .async_status import AsyncStatus
|
|
8
6
|
from .device import Device
|
|
7
|
+
from .signal import SignalR
|
|
8
|
+
from .utils import merge_gathered_dicts
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class StandardReadable(Device, Readable, Configurable, Stageable):
|