ophyd-async 0.3a1__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 +1 -1
- ophyd_async/core/__init__.py +12 -2
- ophyd_async/core/_providers.py +3 -1
- ophyd_async/core/detector.py +65 -38
- ophyd_async/core/device.py +8 -0
- ophyd_async/core/flyer.py +10 -19
- ophyd_async/core/signal.py +36 -17
- ophyd_async/core/signal_backend.py +5 -2
- ophyd_async/core/sim_signal_backend.py +28 -16
- ophyd_async/core/standard_readable.py +4 -2
- ophyd_async/core/utils.py +18 -1
- ophyd_async/epics/_backend/_aioca.py +13 -11
- ophyd_async/epics/_backend/_p4p.py +19 -16
- ophyd_async/epics/_backend/common.py +16 -11
- ophyd_async/epics/areadetector/__init__.py +4 -0
- ophyd_async/epics/areadetector/aravis.py +69 -0
- ophyd_async/epics/areadetector/controllers/aravis_controller.py +73 -0
- ophyd_async/epics/areadetector/controllers/pilatus_controller.py +36 -24
- 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 +4 -4
- ophyd_async/epics/areadetector/writers/hdf_writer.py +6 -1
- 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/signal.py +26 -9
- ophyd_async/panda/__init__.py +17 -6
- ophyd_async/panda/_common_blocks.py +49 -0
- ophyd_async/panda/_hdf_panda.py +48 -0
- ophyd_async/panda/{panda_controller.py → _panda_controller.py} +3 -7
- ophyd_async/panda/_trigger.py +39 -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.3a1.dist-info → ophyd_async-0.3a2.dist-info}/METADATA +19 -75
- ophyd_async-0.3a2.dist-info/RECORD +76 -0
- ophyd_async/epics/pvi.py +0 -70
- ophyd_async/panda/panda.py +0 -241
- ophyd_async-0.3a1.dist-info/RECORD +0 -56
- /ophyd_async/panda/{table.py → _table.py} +0 -0
- /ophyd_async/panda/{utils.py → _utils.py} +0 -0
- {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/LICENSE +0 -0
- {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/WHEEL +0 -0
- {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.3a1.dist-info → ophyd_async-0.3a2.dist-info}/top_level.txt +0 -0
ophyd_async/__init__.py
CHANGED
ophyd_async/_version.py
CHANGED
ophyd_async/core/__init__.py
CHANGED
|
@@ -6,7 +6,13 @@ from ._providers import (
|
|
|
6
6
|
StaticDirectoryProvider,
|
|
7
7
|
)
|
|
8
8
|
from .async_status import AsyncStatus
|
|
9
|
-
from .detector import
|
|
9
|
+
from .detector import (
|
|
10
|
+
DetectorControl,
|
|
11
|
+
DetectorTrigger,
|
|
12
|
+
DetectorWriter,
|
|
13
|
+
StandardDetector,
|
|
14
|
+
TriggerInfo,
|
|
15
|
+
)
|
|
10
16
|
from .device import Device, DeviceCollector, DeviceVector
|
|
11
17
|
from .device_save_loader import (
|
|
12
18
|
get_signal_values,
|
|
@@ -17,7 +23,7 @@ from .device_save_loader import (
|
|
|
17
23
|
set_signal_values,
|
|
18
24
|
walk_rw_signals,
|
|
19
25
|
)
|
|
20
|
-
from .flyer import HardwareTriggeredFlyable,
|
|
26
|
+
from .flyer import HardwareTriggeredFlyable, TriggerLogic
|
|
21
27
|
from .signal import (
|
|
22
28
|
Signal,
|
|
23
29
|
SignalR,
|
|
@@ -29,6 +35,8 @@ from .signal import (
|
|
|
29
35
|
set_sim_callback,
|
|
30
36
|
set_sim_put_proceeds,
|
|
31
37
|
set_sim_value,
|
|
38
|
+
soft_signal_r_and_backend,
|
|
39
|
+
soft_signal_rw,
|
|
32
40
|
wait_for_value,
|
|
33
41
|
)
|
|
34
42
|
from .signal_backend import SignalBackend
|
|
@@ -61,6 +69,8 @@ __all__ = [
|
|
|
61
69
|
"SignalW",
|
|
62
70
|
"SignalRW",
|
|
63
71
|
"SignalX",
|
|
72
|
+
"soft_signal_r_and_backend",
|
|
73
|
+
"soft_signal_rw",
|
|
64
74
|
"observe_value",
|
|
65
75
|
"set_and_wait_for_value",
|
|
66
76
|
"set_sim_callback",
|
ophyd_async/core/_providers.py
CHANGED
|
@@ -39,8 +39,10 @@ class StaticDirectoryProvider(DirectoryProvider):
|
|
|
39
39
|
directory_path: Union[str, Path],
|
|
40
40
|
filename_prefix: str = "",
|
|
41
41
|
filename_suffix: str = "",
|
|
42
|
-
resource_dir: Path =
|
|
42
|
+
resource_dir: Optional[Path] = None,
|
|
43
43
|
) -> None:
|
|
44
|
+
if resource_dir is None:
|
|
45
|
+
resource_dir = Path(".")
|
|
44
46
|
if isinstance(directory_path, str):
|
|
45
47
|
directory_path = Path(directory_path)
|
|
46
48
|
self._directory_info = DirectoryInfo(
|
ophyd_async/core/detector.py
CHANGED
|
@@ -10,6 +10,7 @@ from typing import (
|
|
|
10
10
|
AsyncIterator,
|
|
11
11
|
Callable,
|
|
12
12
|
Dict,
|
|
13
|
+
Generic,
|
|
13
14
|
List,
|
|
14
15
|
Optional,
|
|
15
16
|
Sequence,
|
|
@@ -18,11 +19,9 @@ from typing import (
|
|
|
18
19
|
|
|
19
20
|
from bluesky.protocols import (
|
|
20
21
|
Collectable,
|
|
21
|
-
Configurable,
|
|
22
22
|
Descriptor,
|
|
23
23
|
Flyable,
|
|
24
24
|
Preparable,
|
|
25
|
-
Readable,
|
|
26
25
|
Reading,
|
|
27
26
|
Stageable,
|
|
28
27
|
StreamAsset,
|
|
@@ -30,6 +29,8 @@ from bluesky.protocols import (
|
|
|
30
29
|
WritesStreamAssets,
|
|
31
30
|
)
|
|
32
31
|
|
|
32
|
+
from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
|
|
33
|
+
|
|
33
34
|
from .async_status import AsyncStatus
|
|
34
35
|
from .device import Device
|
|
35
36
|
from .signal import SignalR
|
|
@@ -39,6 +40,8 @@ T = TypeVar("T")
|
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
class DetectorTrigger(str, Enum):
|
|
43
|
+
"""Type of mechanism for triggering a detector to take frames"""
|
|
44
|
+
|
|
42
45
|
#: Detector generates internal trigger for given rate
|
|
43
46
|
internal = "internal"
|
|
44
47
|
#: Expect a series of arbitrary length trigger signals
|
|
@@ -51,6 +54,8 @@ class DetectorTrigger(str, Enum):
|
|
|
51
54
|
|
|
52
55
|
@dataclass(frozen=True)
|
|
53
56
|
class TriggerInfo:
|
|
57
|
+
"""Minimal set of information required to setup triggering on a detector"""
|
|
58
|
+
|
|
54
59
|
#: Number of triggers that will be sent
|
|
55
60
|
num: int
|
|
56
61
|
#: Sort of triggers that will be sent
|
|
@@ -62,6 +67,11 @@ class TriggerInfo:
|
|
|
62
67
|
|
|
63
68
|
|
|
64
69
|
class DetectorControl(ABC):
|
|
70
|
+
"""
|
|
71
|
+
Classes implementing this interface should hold the logic for
|
|
72
|
+
arming and disarming a detector
|
|
73
|
+
"""
|
|
74
|
+
|
|
65
75
|
@abstractmethod
|
|
66
76
|
def get_deadtime(self, exposure: float) -> float:
|
|
67
77
|
"""For a given exposure, how long should the time between exposures be"""
|
|
@@ -73,17 +83,32 @@ class DetectorControl(ABC):
|
|
|
73
83
|
trigger: DetectorTrigger = DetectorTrigger.internal,
|
|
74
84
|
exposure: Optional[float] = None,
|
|
75
85
|
) -> AsyncStatus:
|
|
76
|
-
"""
|
|
86
|
+
"""
|
|
87
|
+
Arm detector, do all necessary steps to prepare detector for triggers.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
num: Expected number of frames
|
|
91
|
+
trigger: Type of trigger for which to prepare the detector. Defaults to
|
|
92
|
+
DetectorTrigger.internal.
|
|
93
|
+
exposure: Exposure time with which to set up the detector. Defaults to None
|
|
94
|
+
if not applicable or the detector is expected to use its previously-set
|
|
95
|
+
exposure time.
|
|
77
96
|
|
|
78
|
-
|
|
97
|
+
Returns:
|
|
98
|
+
AsyncStatus: Status representing the arm operation. This function returning
|
|
99
|
+
represents the start of the arm. The returned status completing means
|
|
100
|
+
the detector is now armed.
|
|
79
101
|
"""
|
|
80
102
|
|
|
81
103
|
@abstractmethod
|
|
82
104
|
async def disarm(self):
|
|
83
|
-
"""Disarm the detector"""
|
|
105
|
+
"""Disarm the detector, return detector to an idle state"""
|
|
84
106
|
|
|
85
107
|
|
|
86
108
|
class DetectorWriter(ABC):
|
|
109
|
+
"""Logic for making a detector write data to somewhere persistent
|
|
110
|
+
(e.g. an HDF5 file)"""
|
|
111
|
+
|
|
87
112
|
@abstractmethod
|
|
88
113
|
async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]:
|
|
89
114
|
"""Open writer and wait for it to be ready for data.
|
|
@@ -100,7 +125,7 @@ class DetectorWriter(ABC):
|
|
|
100
125
|
def observe_indices_written(
|
|
101
126
|
self, timeout=DEFAULT_TIMEOUT
|
|
102
127
|
) -> AsyncGenerator[int, None]:
|
|
103
|
-
"""Yield
|
|
128
|
+
"""Yield the index of each frame (or equivalent data point) as it is written"""
|
|
104
129
|
|
|
105
130
|
@abstractmethod
|
|
106
131
|
async def get_indices_written(self) -> int:
|
|
@@ -112,27 +137,24 @@ class DetectorWriter(ABC):
|
|
|
112
137
|
|
|
113
138
|
@abstractmethod
|
|
114
139
|
async def close(self) -> None:
|
|
115
|
-
"""Close writer
|
|
140
|
+
"""Close writer, blocks until I/O is complete"""
|
|
116
141
|
|
|
117
142
|
|
|
118
143
|
class StandardDetector(
|
|
119
144
|
Device,
|
|
120
145
|
Stageable,
|
|
121
|
-
|
|
122
|
-
|
|
146
|
+
AsyncConfigurable,
|
|
147
|
+
AsyncReadable,
|
|
123
148
|
Triggerable,
|
|
124
149
|
Preparable,
|
|
125
150
|
Flyable,
|
|
126
151
|
Collectable,
|
|
127
152
|
WritesStreamAssets,
|
|
153
|
+
Generic[T],
|
|
128
154
|
):
|
|
129
|
-
"""
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
DetectorData, to dictate how the detector will be controlled (i.e. arming and
|
|
133
|
-
disarming) as well as how the detector data will be written (i.e. opening and
|
|
134
|
-
closing the writer, and handling data writing indices).
|
|
135
|
-
|
|
155
|
+
"""
|
|
156
|
+
Useful detector base class for step and fly scanning detectors.
|
|
157
|
+
Aggregates controller and writer logic together.
|
|
136
158
|
"""
|
|
137
159
|
|
|
138
160
|
def __init__(
|
|
@@ -144,14 +166,18 @@ class StandardDetector(
|
|
|
144
166
|
writer_timeout: float = DEFAULT_TIMEOUT,
|
|
145
167
|
) -> None:
|
|
146
168
|
"""
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
169
|
+
Constructor
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
controller: Logic for arming and disarming the detector
|
|
173
|
+
writer: Logic for making the detector write persistent data
|
|
174
|
+
config_sigs: Signals to read when describe and read
|
|
175
|
+
configuration are called. Defaults to ().
|
|
176
|
+
name: Device name. Defaults to "".
|
|
177
|
+
writer_timeout: Timeout for frame writing to start, if the
|
|
178
|
+
timeout is reached, ophyd-async assumes the detector
|
|
179
|
+
has a problem and raises an error.
|
|
180
|
+
Defaults to DEFAULT_TIMEOUT.
|
|
155
181
|
"""
|
|
156
182
|
self._controller = controller
|
|
157
183
|
self._writer = writer
|
|
@@ -180,12 +206,12 @@ class StandardDetector(
|
|
|
180
206
|
|
|
181
207
|
@AsyncStatus.wrap
|
|
182
208
|
async def stage(self) -> None:
|
|
183
|
-
|
|
184
|
-
await self.
|
|
209
|
+
# Disarm the detector, stop filewriting, and open file for writing.
|
|
210
|
+
await self._check_config_sigs()
|
|
185
211
|
await asyncio.gather(self.writer.close(), self.controller.disarm())
|
|
186
212
|
self._describe = await self.writer.open()
|
|
187
213
|
|
|
188
|
-
async def
|
|
214
|
+
async def _check_config_sigs(self):
|
|
189
215
|
"""Checks configuration signals are named and connected."""
|
|
190
216
|
for signal in self._config_sigs:
|
|
191
217
|
if signal._name == "":
|
|
@@ -202,7 +228,7 @@ class StandardDetector(
|
|
|
202
228
|
|
|
203
229
|
@AsyncStatus.wrap
|
|
204
230
|
async def unstage(self) -> None:
|
|
205
|
-
|
|
231
|
+
# Stop data writing.
|
|
206
232
|
await self.writer.close()
|
|
207
233
|
|
|
208
234
|
async def read_configuration(self) -> Dict[str, Reading]:
|
|
@@ -212,16 +238,15 @@ class StandardDetector(
|
|
|
212
238
|
return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
|
|
213
239
|
|
|
214
240
|
async def read(self) -> Dict[str, Reading]:
|
|
215
|
-
"""Read the detector"""
|
|
216
241
|
# All data is in StreamResources, not Events, so nothing to output here
|
|
217
242
|
return {}
|
|
218
243
|
|
|
219
|
-
def describe(self) -> Dict[str, Descriptor]:
|
|
244
|
+
async def describe(self) -> Dict[str, Descriptor]:
|
|
220
245
|
return self._describe
|
|
221
246
|
|
|
222
247
|
@AsyncStatus.wrap
|
|
223
248
|
async def trigger(self) -> None:
|
|
224
|
-
|
|
249
|
+
# Arm the detector and wait for it to finish.
|
|
225
250
|
indices_written = await self.writer.get_indices_written()
|
|
226
251
|
written_status = await self.controller.arm(
|
|
227
252
|
num=1,
|
|
@@ -240,11 +265,12 @@ class StandardDetector(
|
|
|
240
265
|
self,
|
|
241
266
|
value: T,
|
|
242
267
|
) -> AsyncStatus:
|
|
243
|
-
|
|
268
|
+
# Just arm detector for the time being
|
|
244
269
|
return AsyncStatus(self._prepare(value))
|
|
245
270
|
|
|
246
271
|
async def _prepare(self, value: T) -> None:
|
|
247
|
-
"""
|
|
272
|
+
"""
|
|
273
|
+
Arm detector.
|
|
248
274
|
|
|
249
275
|
Prepare the detector with trigger information. This is determined at and passed
|
|
250
276
|
in from the plan level.
|
|
@@ -253,6 +279,9 @@ class StandardDetector(
|
|
|
253
279
|
trigger information determined in trigger.
|
|
254
280
|
|
|
255
281
|
To do: Unify prepare to be use for both fly and step scans.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
value: TriggerInfo describing how to trigger the detector
|
|
256
285
|
"""
|
|
257
286
|
assert type(value) is TriggerInfo
|
|
258
287
|
self._trigger_info = value
|
|
@@ -307,11 +336,9 @@ class StandardDetector(
|
|
|
307
336
|
async def collect_asset_docs(
|
|
308
337
|
self, index: Optional[int] = None
|
|
309
338
|
) -> AsyncIterator[StreamAsset]:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
retrieved for stepscans.
|
|
314
|
-
"""
|
|
339
|
+
# Collect stream datum documents for all indices written.
|
|
340
|
+
# The index is optional, and provided for fly scans, however this needs to be
|
|
341
|
+
# retrieved for step scans.
|
|
315
342
|
if not index:
|
|
316
343
|
index = await self.writer.get_indices_written()
|
|
317
344
|
async for doc in self.writer.collect_stream_docs(index):
|
ophyd_async/core/device.py
CHANGED
|
@@ -82,6 +82,14 @@ VT = TypeVar("VT", bound=Device)
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
class DeviceVector(Dict[int, VT], Device):
|
|
85
|
+
"""
|
|
86
|
+
Defines device components with indices.
|
|
87
|
+
|
|
88
|
+
In the below example, foos becomes a dictionary on the parent device
|
|
89
|
+
at runtime, so parent.foos[2] returns a FooDevice. For example usage see
|
|
90
|
+
:class:`~ophyd_async.epics.demo.DynamicSensorGroup`
|
|
91
|
+
"""
|
|
92
|
+
|
|
85
93
|
def children(self) -> Generator[Tuple[str, Device], None, None]:
|
|
86
94
|
for attr_name, attr in self.items():
|
|
87
95
|
if isinstance(attr, Device):
|
ophyd_async/core/flyer.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import Dict, Generic,
|
|
2
|
+
from typing import Dict, Generic, Sequence, TypeVar
|
|
3
3
|
|
|
4
4
|
from bluesky.protocols import Descriptor, Flyable, Preparable, Reading, Stageable
|
|
5
5
|
|
|
6
6
|
from .async_status import AsyncStatus
|
|
7
|
-
from .detector import TriggerInfo
|
|
8
7
|
from .device import Device
|
|
9
8
|
from .signal import SignalR
|
|
10
9
|
from .utils import merge_gathered_dicts
|
|
@@ -13,18 +12,18 @@ T = TypeVar("T")
|
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
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
15
|
@abstractmethod
|
|
21
16
|
async def prepare(self, value: T):
|
|
22
17
|
"""Move to the start of the flyscan"""
|
|
23
18
|
|
|
24
19
|
@abstractmethod
|
|
25
|
-
async def
|
|
20
|
+
async def kickoff(self):
|
|
26
21
|
"""Start the flyscan"""
|
|
27
22
|
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def complete(self):
|
|
25
|
+
"""Block until the flyscan is done"""
|
|
26
|
+
|
|
28
27
|
@abstractmethod
|
|
29
28
|
async def stop(self):
|
|
30
29
|
"""Stop flying and wait everything to be stopped"""
|
|
@@ -45,19 +44,12 @@ class HardwareTriggeredFlyable(
|
|
|
45
44
|
):
|
|
46
45
|
self._trigger_logic = trigger_logic
|
|
47
46
|
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
47
|
super().__init__(name=name)
|
|
52
48
|
|
|
53
49
|
@property
|
|
54
50
|
def trigger_logic(self) -> TriggerLogic[T]:
|
|
55
51
|
return self._trigger_logic
|
|
56
52
|
|
|
57
|
-
@property
|
|
58
|
-
def trigger_info(self) -> Optional[TriggerInfo]:
|
|
59
|
-
return self._trigger_info
|
|
60
|
-
|
|
61
53
|
@AsyncStatus.wrap
|
|
62
54
|
async def stage(self) -> None:
|
|
63
55
|
await self.unstage()
|
|
@@ -71,17 +63,16 @@ class HardwareTriggeredFlyable(
|
|
|
71
63
|
return AsyncStatus(self._prepare(value))
|
|
72
64
|
|
|
73
65
|
async def _prepare(self, value: T) -> None:
|
|
74
|
-
self._trigger_info = self._trigger_logic.trigger_info(value)
|
|
75
66
|
# Move to start and setup the flyscan
|
|
76
67
|
await self._trigger_logic.prepare(value)
|
|
77
68
|
|
|
78
69
|
@AsyncStatus.wrap
|
|
79
70
|
async def kickoff(self) -> None:
|
|
80
|
-
|
|
71
|
+
await self._trigger_logic.kickoff()
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
73
|
+
@AsyncStatus.wrap
|
|
74
|
+
async def complete(self) -> None:
|
|
75
|
+
await self._trigger_logic.complete()
|
|
85
76
|
|
|
86
77
|
async def describe_configuration(self) -> Dict[str, Descriptor]:
|
|
87
78
|
return await merge_gathered_dicts(
|
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,24 +46,18 @@ 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
|
-
@property
|
|
55
|
-
def name(self) -> str:
|
|
56
|
-
return self._name
|
|
57
|
-
|
|
58
|
-
def set_name(self, name: str = ""):
|
|
59
|
-
self._name = name
|
|
60
|
-
|
|
61
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
|
|
@@ -72,7 +67,7 @@ class Signal(Device, Generic[T]):
|
|
|
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,6 +248,30 @@ def set_sim_callback(signal: Signal[T], callback: ReadingValueCallback[T]) -> No
|
|
|
253
248
|
return _sim_backends[signal].set_callback(callback)
|
|
254
249
|
|
|
255
250
|
|
|
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
|
+
|
|
256
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
|
|
|
@@ -13,7 +13,10 @@ 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
22
|
async def connect(self, timeout: float = DEFAULT_TIMEOUT):
|
|
@@ -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
|
|
@@ -2,13 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import inspect
|
|
5
|
-
import re
|
|
6
5
|
import time
|
|
7
6
|
from collections import abc
|
|
8
7
|
from dataclasses import dataclass
|
|
9
8
|
from enum import Enum
|
|
10
9
|
from typing import Any, Dict, Generic, Optional, Type, Union, cast, get_origin
|
|
11
10
|
|
|
11
|
+
import numpy as np
|
|
12
12
|
from bluesky.protocols import Descriptor, Dtype, Reading
|
|
13
13
|
|
|
14
14
|
from .signal_backend import SignalBackend
|
|
@@ -37,11 +37,16 @@ class SimConverter(Generic[T]):
|
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
def descriptor(self, source: str, value) -> Descriptor:
|
|
40
|
+
dtype = type(value)
|
|
41
|
+
if np.issubdtype(dtype, np.integer):
|
|
42
|
+
dtype = int
|
|
43
|
+
elif np.issubdtype(dtype, np.floating):
|
|
44
|
+
dtype = float
|
|
40
45
|
assert (
|
|
41
|
-
|
|
46
|
+
dtype in primitive_dtypes
|
|
42
47
|
), f"invalid converter for value of type {type(value)}"
|
|
43
|
-
|
|
44
|
-
return
|
|
48
|
+
dtype_name = primitive_dtypes[dtype]
|
|
49
|
+
return {"source": source, "dtype": dtype_name, "shape": []}
|
|
45
50
|
|
|
46
51
|
def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
|
|
47
52
|
if datatype is None:
|
|
@@ -52,7 +57,7 @@ class SimConverter(Generic[T]):
|
|
|
52
57
|
|
|
53
58
|
class SimArrayConverter(SimConverter):
|
|
54
59
|
def descriptor(self, source: str, value) -> Descriptor:
|
|
55
|
-
return
|
|
60
|
+
return {"source": source, "dtype": "array", "shape": [len(value)]}
|
|
56
61
|
|
|
57
62
|
def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
|
|
58
63
|
if datatype is None:
|
|
@@ -76,9 +81,7 @@ class SimEnumConverter(SimConverter):
|
|
|
76
81
|
|
|
77
82
|
def descriptor(self, source: str, value) -> Descriptor:
|
|
78
83
|
choices = [e.value for e in self.enum_class]
|
|
79
|
-
return
|
|
80
|
-
source=source, dtype="string", shape=[], choices=choices
|
|
81
|
-
) # type: ignore
|
|
84
|
+
return {"source": source, "dtype": "string", "shape": [], "choices": choices} # type: ignore
|
|
82
85
|
|
|
83
86
|
def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
|
|
84
87
|
if datatype is None:
|
|
@@ -109,23 +112,32 @@ class SimSignalBackend(SignalBackend[T]):
|
|
|
109
112
|
"""An simulated backend to a Signal, created with ``Signal.connect(sim=True)``"""
|
|
110
113
|
|
|
111
114
|
_value: T
|
|
112
|
-
_initial_value: T
|
|
115
|
+
_initial_value: Optional[T]
|
|
113
116
|
_timestamp: float
|
|
114
117
|
_severity: int
|
|
115
118
|
|
|
116
|
-
def __init__(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
datatype: Optional[Type[T]],
|
|
122
|
+
initial_value: Optional[T] = None,
|
|
123
|
+
) -> None:
|
|
119
124
|
self.datatype = datatype
|
|
120
|
-
self.pv = source
|
|
121
125
|
self.converter: SimConverter = DisconnectedSimConverter()
|
|
126
|
+
self._initial_value = initial_value
|
|
122
127
|
self.put_proceeds = asyncio.Event()
|
|
123
128
|
self.put_proceeds.set()
|
|
124
129
|
self.callback: Optional[ReadingValueCallback[T]] = None
|
|
125
130
|
|
|
131
|
+
def source(self, name: str) -> str:
|
|
132
|
+
return f"soft://{name}"
|
|
133
|
+
|
|
126
134
|
async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
|
|
127
135
|
self.converter = make_converter(self.datatype)
|
|
128
|
-
self._initial_value
|
|
136
|
+
if self._initial_value is None:
|
|
137
|
+
self._initial_value = self.converter.make_initial_value(self.datatype)
|
|
138
|
+
else:
|
|
139
|
+
# convert potentially unconverted initial value passed to init method
|
|
140
|
+
self._initial_value = self.converter.write_value(self._initial_value)
|
|
129
141
|
self._severity = 0
|
|
130
142
|
|
|
131
143
|
await self.put(None)
|
|
@@ -152,8 +164,8 @@ class SimSignalBackend(SignalBackend[T]):
|
|
|
152
164
|
if self.callback:
|
|
153
165
|
self.callback(reading, self._value)
|
|
154
166
|
|
|
155
|
-
async def get_descriptor(self) -> Descriptor:
|
|
156
|
-
return self.converter.descriptor(
|
|
167
|
+
async def get_descriptor(self, source: str) -> Descriptor:
|
|
168
|
+
return self.converter.descriptor(source, self._value)
|
|
157
169
|
|
|
158
170
|
async def get_reading(self) -> Reading:
|
|
159
171
|
return self.converter.reading(self._value, self._timestamp, self._severity)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from typing import Dict, Sequence, Tuple
|
|
2
2
|
|
|
3
|
-
from bluesky.protocols import
|
|
3
|
+
from bluesky.protocols import Descriptor, Reading, Stageable
|
|
4
|
+
|
|
5
|
+
from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
|
|
4
6
|
|
|
5
7
|
from .async_status import AsyncStatus
|
|
6
8
|
from .device import Device
|
|
@@ -8,7 +10,7 @@ from .signal import SignalR
|
|
|
8
10
|
from .utils import merge_gathered_dicts
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
class StandardReadable(Device,
|
|
13
|
+
class StandardReadable(Device, AsyncReadable, AsyncConfigurable, Stageable):
|
|
12
14
|
"""Device that owns its children and provides useful default behavior.
|
|
13
15
|
|
|
14
16
|
- When its name is set it renames child Devices
|