ophyd-async 0.8.0a5__py3-none-any.whl → 0.9.0__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 +17 -46
- ophyd_async/core/_detector.py +68 -44
- ophyd_async/core/_device.py +120 -79
- ophyd_async/core/_device_filler.py +17 -8
- ophyd_async/core/_flyer.py +2 -2
- ophyd_async/core/_protocol.py +0 -28
- ophyd_async/core/_readable.py +30 -23
- ophyd_async/core/_settings.py +104 -0
- ophyd_async/core/_signal.py +164 -151
- ophyd_async/core/_signal_backend.py +4 -1
- ophyd_async/core/_soft_signal_backend.py +2 -1
- ophyd_async/core/_table.py +27 -14
- ophyd_async/core/_utils.py +30 -5
- ophyd_async/core/_yaml_settings.py +64 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +49 -0
- ophyd_async/epics/adandor/_andor_io.py +36 -0
- ophyd_async/epics/adaravis/__init__.py +3 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -37
- ophyd_async/epics/adaravis/_aravis_controller.py +21 -30
- ophyd_async/epics/adaravis/_aravis_io.py +4 -4
- ophyd_async/epics/adcore/__init__.py +15 -8
- ophyd_async/epics/adcore/_core_detector.py +41 -0
- ophyd_async/epics/adcore/_core_io.py +56 -31
- ophyd_async/epics/adcore/_core_logic.py +99 -84
- ophyd_async/epics/adcore/_core_writer.py +219 -0
- ophyd_async/epics/adcore/_hdf_writer.py +33 -59
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +5 -4
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +37 -36
- ophyd_async/epics/adkinetix/_kinetix.py +29 -24
- ophyd_async/epics/adkinetix/_kinetix_controller.py +15 -27
- ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
- ophyd_async/epics/adpilatus/__init__.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +47 -25
- ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
- ophyd_async/epics/adsimdetector/__init__.py +3 -3
- ophyd_async/epics/adsimdetector/_sim.py +33 -17
- ophyd_async/epics/advimba/_vimba.py +23 -23
- ophyd_async/epics/advimba/_vimba_controller.py +21 -35
- ophyd_async/epics/advimba/_vimba_io.py +23 -23
- ophyd_async/epics/core/_aioca.py +52 -21
- ophyd_async/epics/core/_p4p.py +59 -16
- ophyd_async/epics/core/_pvi_connector.py +4 -2
- ophyd_async/epics/core/_signal.py +9 -2
- ophyd_async/epics/core/_util.py +10 -1
- ophyd_async/epics/eiger/_eiger_controller.py +10 -5
- ophyd_async/epics/eiger/_eiger_io.py +3 -3
- ophyd_async/epics/motor.py +26 -15
- ophyd_async/epics/sim/_ioc.py +29 -0
- ophyd_async/epics/{demo → sim}/_mover.py +12 -6
- ophyd_async/epics/{demo → sim}/_sensor.py +2 -2
- ophyd_async/epics/testing/__init__.py +24 -0
- ophyd_async/epics/testing/_example_ioc.py +91 -0
- ophyd_async/epics/testing/_utils.py +50 -0
- ophyd_async/epics/testing/test_records.db +174 -0
- ophyd_async/epics/testing/test_records_pva.db +177 -0
- ophyd_async/fastcs/core.py +2 -2
- ophyd_async/fastcs/panda/__init__.py +0 -2
- ophyd_async/fastcs/panda/_block.py +9 -9
- ophyd_async/fastcs/panda/_control.py +9 -4
- ophyd_async/fastcs/panda/_hdf_panda.py +7 -2
- ophyd_async/fastcs/panda/_table.py +4 -1
- ophyd_async/fastcs/panda/_trigger.py +7 -7
- ophyd_async/plan_stubs/__init__.py +14 -0
- ophyd_async/plan_stubs/_ensure_connected.py +11 -17
- ophyd_async/plan_stubs/_fly.py +2 -2
- ophyd_async/plan_stubs/_nd_attributes.py +7 -5
- ophyd_async/plan_stubs/_panda.py +13 -0
- ophyd_async/plan_stubs/_settings.py +125 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +19 -0
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
- ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +34 -32
- ophyd_async/tango/__init__.py +0 -43
- ophyd_async/tango/{signal → core}/__init__.py +7 -2
- ophyd_async/tango/{base_devices → core}/_base_device.py +38 -64
- ophyd_async/tango/{signal → core}/_signal.py +16 -4
- ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
- ophyd_async/tango/{signal → core}/_tango_transport.py +13 -15
- ophyd_async/tango/{demo → sim}/_counter.py +6 -7
- ophyd_async/tango/{demo → sim}/_mover.py +13 -9
- ophyd_async/testing/__init__.py +52 -0
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +176 -0
- ophyd_async/{core → testing}/_mock_signal_utils.py +15 -11
- ophyd_async/testing/_one_of_everything.py +126 -0
- ophyd_async/testing/_wait_for_pending.py +22 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/METADATA +50 -48
- ophyd_async-0.9.0.dist-info/RECORD +129 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async/tango/base_devices/__init__.py +0 -4
- ophyd_async-0.8.0a5.dist-info/RECORD +0 -112
- ophyd_async-0.8.0a5.dist-info/entry_points.txt +0 -2
- /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
- /ophyd_async/epics/{demo → sim}/mover.db +0 -0
- /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
- /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import bluesky.plan_stubs as bps
|
|
8
|
+
import numpy as np
|
|
9
|
+
from bluesky.utils import MsgGenerator, plan
|
|
10
|
+
|
|
11
|
+
from ophyd_async.core import (
|
|
12
|
+
Device,
|
|
13
|
+
Settings,
|
|
14
|
+
SettingsProvider,
|
|
15
|
+
SignalRW,
|
|
16
|
+
T,
|
|
17
|
+
walk_rw_signals,
|
|
18
|
+
)
|
|
19
|
+
from ophyd_async.core._table import Table
|
|
20
|
+
|
|
21
|
+
from ._wait_for_awaitable import wait_for_awaitable
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@plan
|
|
25
|
+
def _get_values_of_signals(
|
|
26
|
+
signals: Mapping[T, SignalRW],
|
|
27
|
+
) -> MsgGenerator[dict[T, Any]]:
|
|
28
|
+
coros = [sig.get_value() for sig in signals.values()]
|
|
29
|
+
values = yield from wait_for_awaitable(asyncio.gather(*coros))
|
|
30
|
+
named_values = dict(zip(signals, values, strict=True))
|
|
31
|
+
return named_values
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@plan
|
|
35
|
+
def get_current_settings(device: Device) -> MsgGenerator[Settings]:
|
|
36
|
+
signals = walk_rw_signals(device)
|
|
37
|
+
named_values = yield from _get_values_of_signals(signals)
|
|
38
|
+
signal_values = {signals[name]: value for name, value in named_values.items()}
|
|
39
|
+
return Settings(device, signal_values)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@plan
|
|
43
|
+
def store_settings(
|
|
44
|
+
provider: SettingsProvider, name: str, device: Device
|
|
45
|
+
) -> MsgGenerator[None]:
|
|
46
|
+
"""Walk a Device for SignalRWs and store their values with a provider associated
|
|
47
|
+
with the given name.
|
|
48
|
+
"""
|
|
49
|
+
signals = walk_rw_signals(device)
|
|
50
|
+
named_values = yield from _get_values_of_signals(signals)
|
|
51
|
+
yield from wait_for_awaitable(provider.store(name, named_values))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@plan
|
|
55
|
+
def retrieve_settings(
|
|
56
|
+
provider: SettingsProvider, name: str, device: Device
|
|
57
|
+
) -> MsgGenerator[Settings]:
|
|
58
|
+
"""Retrieve named Settings for a Device from a provider."""
|
|
59
|
+
named_values = yield from wait_for_awaitable(provider.retrieve(name))
|
|
60
|
+
signals = walk_rw_signals(device)
|
|
61
|
+
unknown_names = set(named_values) - set(signals)
|
|
62
|
+
if unknown_names:
|
|
63
|
+
raise NameError(f"Unknown signal names {sorted(unknown_names)}")
|
|
64
|
+
signal_values = {signals[name]: value for name, value in named_values.items()}
|
|
65
|
+
return Settings(device, signal_values)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@plan
|
|
69
|
+
def apply_settings(settings: Settings) -> MsgGenerator[None]:
|
|
70
|
+
"""Set every SignalRW to the given value in Settings. If value is None ignore it."""
|
|
71
|
+
signal_values = {
|
|
72
|
+
signal: value for signal, value in settings.items() if value is not None
|
|
73
|
+
}
|
|
74
|
+
if signal_values:
|
|
75
|
+
for signal, value in signal_values.items():
|
|
76
|
+
yield from bps.abs_set(signal, value, group="apply_settings")
|
|
77
|
+
yield from bps.wait("apply_settings")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@plan
|
|
81
|
+
def apply_settings_if_different(
|
|
82
|
+
settings: Settings,
|
|
83
|
+
apply_plan: Callable[[Settings], MsgGenerator[None]],
|
|
84
|
+
current_settings: Settings | None = None,
|
|
85
|
+
) -> MsgGenerator[None]:
|
|
86
|
+
"""Set every SignalRW in settings to its given value if it is different to the
|
|
87
|
+
current value.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
apply_plan:
|
|
92
|
+
A device specific plan which takes the Settings to apply and applies them to
|
|
93
|
+
the Device. Used to add device specific ordering to setting the signals.
|
|
94
|
+
current_settings:
|
|
95
|
+
If given, should be a superset of settings containing the current value of
|
|
96
|
+
the Settings in the Device. If not given it will be created by reading just
|
|
97
|
+
the signals given in settings.
|
|
98
|
+
"""
|
|
99
|
+
if current_settings is None:
|
|
100
|
+
# If we aren't give the current settings, then get the
|
|
101
|
+
# values of just the signals we were asked to change.
|
|
102
|
+
# This allows us to use this plan with Settings for a subset
|
|
103
|
+
# of signals in the Device without retrieving them all
|
|
104
|
+
signal_values = yield from _get_values_of_signals(
|
|
105
|
+
{sig: sig for sig in settings}
|
|
106
|
+
)
|
|
107
|
+
current_settings = Settings(settings.device, signal_values)
|
|
108
|
+
|
|
109
|
+
def _is_different(current, required) -> bool:
|
|
110
|
+
if isinstance(current, Table):
|
|
111
|
+
current = current.model_dump()
|
|
112
|
+
if isinstance(required, Table):
|
|
113
|
+
required = required.model_dump()
|
|
114
|
+
return current.keys() != required.keys() or any(
|
|
115
|
+
_is_different(current[k], required[k]) for k in current
|
|
116
|
+
)
|
|
117
|
+
elif isinstance(current, np.ndarray):
|
|
118
|
+
return not np.array_equal(current, required)
|
|
119
|
+
else:
|
|
120
|
+
return current != required
|
|
121
|
+
|
|
122
|
+
settings_to_change, _ = settings.partition(
|
|
123
|
+
lambda sig: _is_different(current_settings[sig], settings[sig])
|
|
124
|
+
)
|
|
125
|
+
yield from apply_plan(settings_to_change)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from collections.abc import Awaitable
|
|
2
|
+
|
|
3
|
+
import bluesky.plan_stubs as bps
|
|
4
|
+
from bluesky.utils import MsgGenerator, plan
|
|
5
|
+
|
|
6
|
+
from ophyd_async.core import T
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@plan
|
|
10
|
+
def wait_for_awaitable(coro: Awaitable[T]) -> MsgGenerator[T]:
|
|
11
|
+
"""Wait for a single awaitable to complete, and return the result."""
|
|
12
|
+
(task,) = yield from bps.wait_for([lambda: coro])
|
|
13
|
+
return task.result()
|
ophyd_async/sim/__init__.py
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from ._pattern_detector import (
|
|
2
|
+
DATA_PATH,
|
|
3
|
+
SUM_PATH,
|
|
4
|
+
PatternDetector,
|
|
5
|
+
PatternDetectorController,
|
|
6
|
+
PatternDetectorWriter,
|
|
7
|
+
PatternGenerator,
|
|
8
|
+
)
|
|
9
|
+
from ._sim_motor import SimMotor
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"DATA_PATH",
|
|
13
|
+
"SUM_PATH",
|
|
14
|
+
"PatternGenerator",
|
|
15
|
+
"PatternDetector",
|
|
16
|
+
"PatternDetectorController",
|
|
17
|
+
"PatternDetectorWriter",
|
|
18
|
+
"SimMotor",
|
|
19
|
+
]
|
ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py
RENAMED
|
@@ -27,8 +27,15 @@ class PatternDetectorController(DetectorController):
|
|
|
27
27
|
)
|
|
28
28
|
|
|
29
29
|
async def arm(self):
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
if not hasattr(self, "_trigger_info"):
|
|
31
|
+
msg = "TriggerInfo information is missing, has 'prepare' been called?"
|
|
32
|
+
raise RuntimeError(msg)
|
|
33
|
+
if not self._trigger_info.livetime:
|
|
34
|
+
msg = "Livetime information is missing in trigger info"
|
|
35
|
+
raise ValueError(msg)
|
|
36
|
+
if not self.period:
|
|
37
|
+
msg = "Period is not set"
|
|
38
|
+
raise ValueError(msg)
|
|
32
39
|
self.task = asyncio.create_task(
|
|
33
40
|
self._coroutine_for_image_writing(
|
|
34
41
|
self._trigger_info.livetime,
|
|
@@ -67,11 +67,14 @@ class PatternGenerator:
|
|
|
67
67
|
|
|
68
68
|
def write_data_to_dataset(self, path: str, data_shape: tuple[int, ...], data):
|
|
69
69
|
"""Write data to named dataset, resizing to fit and flushing after."""
|
|
70
|
-
|
|
70
|
+
if not self._handle_for_h5_file:
|
|
71
|
+
msg = "No file has been opened!"
|
|
72
|
+
raise OSError(msg)
|
|
73
|
+
|
|
71
74
|
dset = self._handle_for_h5_file[path]
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
if not isinstance(dset, h5py.Dataset):
|
|
76
|
+
msg = f"Expected {path} to be a dataset, got {type(dset).__name__}"
|
|
77
|
+
raise TypeError(msg)
|
|
75
78
|
dset.resize((self.image_counter + 1,) + data_shape)
|
|
76
79
|
dset[self.image_counter] = data
|
|
77
80
|
dset.flush()
|
|
@@ -114,7 +117,9 @@ class PatternGenerator:
|
|
|
114
117
|
|
|
115
118
|
self._handle_for_h5_file = h5py.File(self.target_path, "w", libver="latest")
|
|
116
119
|
|
|
117
|
-
|
|
120
|
+
if not self._handle_for_h5_file:
|
|
121
|
+
msg = f"Problem opening file {self.target_path}"
|
|
122
|
+
raise OSError(msg)
|
|
118
123
|
|
|
119
124
|
self._handle_for_h5_file.create_dataset(
|
|
120
125
|
name=DATA_PATH,
|
|
@@ -184,7 +189,9 @@ class PatternGenerator:
|
|
|
184
189
|
# cannot get the full filename the HDF writer will write
|
|
185
190
|
# until the first frame comes in
|
|
186
191
|
if not self._hdf_stream_provider:
|
|
187
|
-
|
|
192
|
+
if self.target_path is None:
|
|
193
|
+
msg = "open file has not been called"
|
|
194
|
+
raise RuntimeError(msg)
|
|
188
195
|
self._hdf_stream_provider = HDFFile(
|
|
189
196
|
self.target_path,
|
|
190
197
|
self._datasets,
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
import contextlib
|
|
3
3
|
import time
|
|
4
4
|
|
|
5
|
+
import numpy as np
|
|
5
6
|
from bluesky.protocols import Movable, Stoppable
|
|
6
7
|
|
|
7
8
|
from ophyd_async.core import (
|
|
@@ -44,22 +45,20 @@ class SimMotor(StandardReadable, Movable, Stoppable):
|
|
|
44
45
|
|
|
45
46
|
async def _move(self, old_position: float, new_position: float, move_time: float):
|
|
46
47
|
start = time.monotonic()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# 10hz update loop
|
|
62
|
-
await asyncio.sleep(0.1)
|
|
48
|
+
# Make an array of relative update times at 10Hz intervals
|
|
49
|
+
update_times = np.arange(0.1, move_time, 0.1)
|
|
50
|
+
# With the end position appended
|
|
51
|
+
update_times = np.concatenate((update_times, [move_time]))
|
|
52
|
+
# Interpolate the [old, new] position array with those update times
|
|
53
|
+
new_positions = np.interp(
|
|
54
|
+
update_times, [0, move_time], [old_position, new_position]
|
|
55
|
+
)
|
|
56
|
+
for update_time, new_position in zip(update_times, new_positions, strict=True):
|
|
57
|
+
# Calculate how long to wait to get there
|
|
58
|
+
relative_time = time.monotonic() - start
|
|
59
|
+
await asyncio.sleep(update_time - relative_time)
|
|
60
|
+
# Update the readback position
|
|
61
|
+
self._user_readback_set(new_position)
|
|
63
62
|
|
|
64
63
|
@WatchableAsyncStatus.wrap
|
|
65
64
|
async def set(self, value: float):
|
|
@@ -75,22 +74,25 @@ class SimMotor(StandardReadable, Movable, Stoppable):
|
|
|
75
74
|
self.velocity.get_value(),
|
|
76
75
|
)
|
|
77
76
|
# If zero velocity, do instant move
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
):
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
77
|
+
if velocity == 0:
|
|
78
|
+
self._user_readback_set(new_position)
|
|
79
|
+
else:
|
|
80
|
+
move_time = abs(new_position - old_position) / velocity
|
|
81
|
+
self._move_status = AsyncStatus(
|
|
82
|
+
self._move(old_position, new_position, move_time)
|
|
83
|
+
)
|
|
84
|
+
# If stop is called then this will raise a CancelledError, ignore it
|
|
85
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
86
|
+
async for current_position in observe_value(
|
|
87
|
+
self.user_readback, done_status=self._move_status
|
|
88
|
+
):
|
|
89
|
+
yield WatcherUpdate(
|
|
90
|
+
current=current_position,
|
|
91
|
+
initial=old_position,
|
|
92
|
+
target=new_position,
|
|
93
|
+
name=self.name,
|
|
94
|
+
unit=units,
|
|
95
|
+
)
|
|
94
96
|
if not self._set_success:
|
|
95
97
|
raise RuntimeError("Motor was stopped")
|
|
96
98
|
|
ophyd_async/tango/__init__.py
CHANGED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
from .base_devices import (
|
|
2
|
-
TangoDevice,
|
|
3
|
-
TangoReadable,
|
|
4
|
-
tango_polling,
|
|
5
|
-
)
|
|
6
|
-
from .signal import (
|
|
7
|
-
AttributeProxy,
|
|
8
|
-
CommandProxy,
|
|
9
|
-
TangoSignalBackend,
|
|
10
|
-
ensure_proper_executor,
|
|
11
|
-
get_dtype_extended,
|
|
12
|
-
get_python_type,
|
|
13
|
-
get_tango_trl,
|
|
14
|
-
get_trl_descriptor,
|
|
15
|
-
infer_python_type,
|
|
16
|
-
infer_signal_type,
|
|
17
|
-
make_backend,
|
|
18
|
-
tango_signal_r,
|
|
19
|
-
tango_signal_rw,
|
|
20
|
-
tango_signal_w,
|
|
21
|
-
tango_signal_x,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
__all__ = [
|
|
25
|
-
"TangoDevice",
|
|
26
|
-
"TangoReadable",
|
|
27
|
-
"tango_polling",
|
|
28
|
-
"TangoSignalBackend",
|
|
29
|
-
"get_python_type",
|
|
30
|
-
"get_dtype_extended",
|
|
31
|
-
"get_trl_descriptor",
|
|
32
|
-
"get_tango_trl",
|
|
33
|
-
"infer_python_type",
|
|
34
|
-
"infer_signal_type",
|
|
35
|
-
"make_backend",
|
|
36
|
-
"AttributeProxy",
|
|
37
|
-
"CommandProxy",
|
|
38
|
-
"ensure_proper_executor",
|
|
39
|
-
"tango_signal_r",
|
|
40
|
-
"tango_signal_rw",
|
|
41
|
-
"tango_signal_w",
|
|
42
|
-
"tango_signal_x",
|
|
43
|
-
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from ._base_device import TangoDevice, TangoPolling
|
|
1
2
|
from ._signal import (
|
|
2
3
|
infer_python_type,
|
|
3
4
|
infer_signal_type,
|
|
@@ -7,6 +8,7 @@ from ._signal import (
|
|
|
7
8
|
tango_signal_w,
|
|
8
9
|
tango_signal_x,
|
|
9
10
|
)
|
|
11
|
+
from ._tango_readable import TangoReadable
|
|
10
12
|
from ._tango_transport import (
|
|
11
13
|
AttributeProxy,
|
|
12
14
|
CommandProxy,
|
|
@@ -18,7 +20,7 @@ from ._tango_transport import (
|
|
|
18
20
|
get_trl_descriptor,
|
|
19
21
|
)
|
|
20
22
|
|
|
21
|
-
__all__ =
|
|
23
|
+
__all__ = [
|
|
22
24
|
"AttributeProxy",
|
|
23
25
|
"CommandProxy",
|
|
24
26
|
"ensure_proper_executor",
|
|
@@ -34,4 +36,7 @@ __all__ = (
|
|
|
34
36
|
"tango_signal_rw",
|
|
35
37
|
"tango_signal_w",
|
|
36
38
|
"tango_signal_x",
|
|
37
|
-
|
|
39
|
+
"TangoDevice",
|
|
40
|
+
"TangoReadable",
|
|
41
|
+
"TangoPolling",
|
|
42
|
+
]
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from ophyd_async.core
|
|
7
|
-
from ophyd_async.tango.signal import (
|
|
8
|
-
TangoSignalBackend,
|
|
9
|
-
infer_python_type,
|
|
10
|
-
infer_signal_type,
|
|
11
|
-
)
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
from ophyd_async.core import Device, DeviceConnector, DeviceFiller, LazyMock
|
|
12
7
|
from tango import DeviceProxy as DeviceProxy
|
|
13
8
|
from tango.asyncio import DeviceProxy as AsyncDeviceProxy
|
|
14
9
|
|
|
10
|
+
from ._signal import TangoSignalBackend, infer_python_type, infer_signal_type
|
|
11
|
+
|
|
15
12
|
T = TypeVar("T")
|
|
16
13
|
|
|
17
14
|
|
|
@@ -32,63 +29,45 @@ class TangoDevice(Device):
|
|
|
32
29
|
|
|
33
30
|
trl: str = ""
|
|
34
31
|
proxy: DeviceProxy | None = None
|
|
35
|
-
_polling: tuple[bool, float, float | None, float | None] = (False, 0.1, None, 0.1)
|
|
36
|
-
_signal_polling: dict[str, tuple[bool, float, float, float]] = {}
|
|
37
|
-
_poll_only_annotated_signals: bool = True
|
|
38
32
|
|
|
39
33
|
def __init__(
|
|
40
34
|
self,
|
|
41
35
|
trl: str | None = None,
|
|
42
36
|
device_proxy: DeviceProxy | None = None,
|
|
37
|
+
support_events: bool = False,
|
|
43
38
|
name: str = "",
|
|
44
39
|
) -> None:
|
|
45
40
|
connector = TangoDeviceConnector(
|
|
46
|
-
trl=trl,
|
|
47
|
-
device_proxy=device_proxy,
|
|
48
|
-
polling=self._polling,
|
|
49
|
-
signal_polling=self._signal_polling,
|
|
41
|
+
trl=trl, device_proxy=device_proxy, support_events=support_events
|
|
50
42
|
)
|
|
51
43
|
super().__init__(name=name, connector=connector)
|
|
52
44
|
|
|
53
45
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
| None = None
|
|
58
|
-
|
|
59
|
-
):
|
|
60
|
-
"""
|
|
61
|
-
Class decorator to configure polling for Tango devices.
|
|
62
|
-
|
|
63
|
-
This decorator allows for the configuration of both device-level and signal-level
|
|
64
|
-
polling for Tango devices. Polling is useful for device servers that do not support
|
|
65
|
-
event-driven updates.
|
|
66
|
-
|
|
67
|
-
Parameters
|
|
68
|
-
----------
|
|
69
|
-
polling : Optional[Union[Tuple[float, float, float],
|
|
70
|
-
Dict[str, Tuple[float, float, float]]]], optional
|
|
71
|
-
Device-level polling configuration as a tuple of three floats representing the
|
|
72
|
-
polling interval, polling timeout, and polling delay. Alternatively,
|
|
73
|
-
a dictionary can be provided to specify signal-level polling configurations
|
|
74
|
-
directly.
|
|
75
|
-
signal_polling : Optional[Dict[str, Tuple[float, float, float]]], optional
|
|
76
|
-
Signal-level polling configuration as a dictionary where keys are signal names
|
|
77
|
-
and values are tuples of three floats representing the polling interval, polling
|
|
78
|
-
timeout, and polling delay.
|
|
79
|
-
"""
|
|
80
|
-
if isinstance(polling, dict):
|
|
81
|
-
signal_polling = polling
|
|
82
|
-
polling = None
|
|
46
|
+
@dataclass
|
|
47
|
+
class TangoPolling(Generic[T]):
|
|
48
|
+
ophyd_polling_period: float = 0.1
|
|
49
|
+
abs_change: T | None = None
|
|
50
|
+
rel_change: T | None = None
|
|
83
51
|
|
|
84
|
-
def decorator(cls):
|
|
85
|
-
if polling is not None:
|
|
86
|
-
cls._polling = (True, *polling)
|
|
87
|
-
if signal_polling is not None:
|
|
88
|
-
cls._signal_polling = {k: (True, *v) for k, v in signal_polling.items()}
|
|
89
|
-
return cls
|
|
90
52
|
|
|
91
|
-
|
|
53
|
+
def fill_backend_with_polling(
|
|
54
|
+
support_events: bool, backend: TangoSignalBackend, annotations: list[Any]
|
|
55
|
+
):
|
|
56
|
+
unhandled = []
|
|
57
|
+
while annotations:
|
|
58
|
+
annotation = annotations.pop(0)
|
|
59
|
+
backend.allow_events(support_events)
|
|
60
|
+
if isinstance(annotation, TangoPolling):
|
|
61
|
+
backend.set_polling(
|
|
62
|
+
not support_events,
|
|
63
|
+
annotation.ophyd_polling_period,
|
|
64
|
+
annotation.abs_change,
|
|
65
|
+
annotation.rel_change,
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
unhandled.append(annotation)
|
|
69
|
+
annotations.extend(unhandled)
|
|
70
|
+
# These leftover annotations will now be handled by the iterator
|
|
92
71
|
|
|
93
72
|
|
|
94
73
|
class TangoDeviceConnector(DeviceConnector):
|
|
@@ -96,13 +75,11 @@ class TangoDeviceConnector(DeviceConnector):
|
|
|
96
75
|
self,
|
|
97
76
|
trl: str | None,
|
|
98
77
|
device_proxy: DeviceProxy | None,
|
|
99
|
-
|
|
100
|
-
signal_polling: dict[str, tuple[bool, float, float, float]],
|
|
78
|
+
support_events: bool,
|
|
101
79
|
) -> None:
|
|
102
80
|
self.trl = trl
|
|
103
81
|
self.proxy = device_proxy
|
|
104
|
-
self.
|
|
105
|
-
self._signal_polling = signal_polling
|
|
82
|
+
self._support_events = support_events
|
|
106
83
|
|
|
107
84
|
def create_children_from_annotations(self, device: Device):
|
|
108
85
|
if not hasattr(self, "filler"):
|
|
@@ -110,11 +87,14 @@ class TangoDeviceConnector(DeviceConnector):
|
|
|
110
87
|
device=device,
|
|
111
88
|
signal_backend_factory=TangoSignalBackend,
|
|
112
89
|
device_connector_factory=lambda: TangoDeviceConnector(
|
|
113
|
-
None, None,
|
|
90
|
+
None, None, self._support_events
|
|
114
91
|
),
|
|
115
92
|
)
|
|
116
93
|
list(self.filler.create_devices_from_annotations(filled=False))
|
|
117
|
-
|
|
94
|
+
for backend, annotations in self.filler.create_signals_from_annotations(
|
|
95
|
+
filled=False
|
|
96
|
+
):
|
|
97
|
+
fill_backend_with_polling(self._support_events, backend, annotations)
|
|
118
98
|
self.filler.check_created()
|
|
119
99
|
|
|
120
100
|
async def connect_mock(self, device: Device, mock: LazyMock):
|
|
@@ -145,12 +125,6 @@ class TangoDeviceConnector(DeviceConnector):
|
|
|
145
125
|
backend = self.filler.fill_child_signal(name, signal_type)
|
|
146
126
|
backend.datatype = await infer_python_type(full_trl, self.proxy)
|
|
147
127
|
backend.set_trl(full_trl)
|
|
148
|
-
if polling := self._signal_polling.get(name, ()):
|
|
149
|
-
backend.set_polling(*polling)
|
|
150
|
-
backend.allow_events(False)
|
|
151
|
-
elif self._polling[0]:
|
|
152
|
-
backend.set_polling(*self._polling)
|
|
153
|
-
backend.allow_events(False)
|
|
154
128
|
# Check that all the requested children have been filled
|
|
155
129
|
self.filler.check_filled(f"{self.trl}: {children}")
|
|
156
130
|
# Set the name of the device to name all children
|
|
@@ -16,11 +16,20 @@ from ophyd_async.core import (
|
|
|
16
16
|
SignalW,
|
|
17
17
|
SignalX,
|
|
18
18
|
)
|
|
19
|
-
from tango import
|
|
19
|
+
from tango import (
|
|
20
|
+
AttrDataFormat,
|
|
21
|
+
AttrWriteType,
|
|
22
|
+
CmdArgType,
|
|
23
|
+
DeviceProxy,
|
|
24
|
+
DevState,
|
|
25
|
+
NonSupportedFeature, # type: ignore
|
|
26
|
+
)
|
|
20
27
|
from tango.asyncio import DeviceProxy as AsyncDeviceProxy
|
|
21
28
|
|
|
22
29
|
from ._tango_transport import TangoSignalBackend, get_python_type
|
|
23
30
|
|
|
31
|
+
logger = logging.getLogger("ophyd_async")
|
|
32
|
+
|
|
24
33
|
|
|
25
34
|
def make_backend(
|
|
26
35
|
datatype: type[SignalDatatypeT] | None,
|
|
@@ -174,8 +183,11 @@ async def infer_signal_type(
|
|
|
174
183
|
else:
|
|
175
184
|
dev_proxy = proxy
|
|
176
185
|
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
try:
|
|
187
|
+
if tr_name in dev_proxy.get_pipe_list():
|
|
188
|
+
raise NotImplementedError("Pipes are not supported")
|
|
189
|
+
except NonSupportedFeature: # type: ignore
|
|
190
|
+
pass
|
|
179
191
|
|
|
180
192
|
if tr_name not in dev_proxy.get_attribute_list():
|
|
181
193
|
if tr_name not in dev_proxy.get_command_list():
|
|
@@ -195,7 +207,7 @@ async def infer_signal_type(
|
|
|
195
207
|
if config.in_type == CmdArgType.DevVoid:
|
|
196
208
|
return SignalX
|
|
197
209
|
elif config.in_type != config.out_type:
|
|
198
|
-
|
|
210
|
+
logger.debug("Commands with different in and out dtypes are not supported")
|
|
199
211
|
return None
|
|
200
212
|
else:
|
|
201
213
|
return SignalRW
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from ophyd_async.core import
|
|
4
|
-
StandardReadable,
|
|
5
|
-
)
|
|
6
|
-
from ophyd_async.tango.base_devices._base_device import TangoDevice
|
|
3
|
+
from ophyd_async.core import StandardReadable
|
|
7
4
|
from tango import DeviceProxy
|
|
8
5
|
|
|
6
|
+
from ._base_device import TangoDevice
|
|
7
|
+
|
|
9
8
|
|
|
10
9
|
class TangoReadable(TangoDevice, StandardReadable):
|
|
11
10
|
"""
|
|
@@ -189,7 +189,7 @@ class AttributeProxy(TangoProxy):
|
|
|
189
189
|
raise TimeoutError(f"{self._name} attr put failed: Timeout") from te
|
|
190
190
|
except DevFailed as de:
|
|
191
191
|
raise RuntimeError(
|
|
192
|
-
f"{self._name} device
|
|
192
|
+
f"{self._name} device failure: {de.args[0].desc}"
|
|
193
193
|
) from de
|
|
194
194
|
|
|
195
195
|
else:
|
|
@@ -210,11 +210,11 @@ class AttributeProxy(TangoProxy):
|
|
|
210
210
|
await asyncio.sleep(A_BIT)
|
|
211
211
|
if to and (time.time() - start_time > to):
|
|
212
212
|
raise TimeoutError(
|
|
213
|
-
f"{self._name} attr put failed:
|
|
213
|
+
f"{self._name} attr put failed: Timeout"
|
|
214
214
|
) from exc
|
|
215
215
|
else:
|
|
216
216
|
raise RuntimeError(
|
|
217
|
-
f"{self._name} device failure:
|
|
217
|
+
f"{self._name} device failure: {exc.args[0].desc}"
|
|
218
218
|
) from exc
|
|
219
219
|
|
|
220
220
|
return AsyncStatus(wait_for_reply(rid, timeout))
|
|
@@ -422,7 +422,7 @@ class CommandProxy(TangoProxy):
|
|
|
422
422
|
raise TimeoutError(f"{self._name} command failed: Timeout") from te
|
|
423
423
|
except DevFailed as de:
|
|
424
424
|
raise RuntimeError(
|
|
425
|
-
f"{self._name} device
|
|
425
|
+
f"{self._name} device failure: {de.args[0].desc}"
|
|
426
426
|
) from de
|
|
427
427
|
|
|
428
428
|
else:
|
|
@@ -446,8 +446,7 @@ class CommandProxy(TangoProxy):
|
|
|
446
446
|
) from de_exc
|
|
447
447
|
else:
|
|
448
448
|
raise RuntimeError(
|
|
449
|
-
f"{self._name} device failure:"
|
|
450
|
-
f" {de_exc.args[0].desc}"
|
|
449
|
+
f"{self._name} device failure: {de_exc.args[0].desc}"
|
|
451
450
|
) from de_exc
|
|
452
451
|
|
|
453
452
|
return AsyncStatus(wait_for_reply(rid, timeout))
|
|
@@ -733,22 +732,21 @@ class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
|
733
732
|
" for which polling is disabled."
|
|
734
733
|
)
|
|
735
734
|
|
|
735
|
+
if callback and self.proxies[self.read_trl].has_subscription(): # type: ignore
|
|
736
|
+
msg = "Cannot set a callback when one is already set"
|
|
737
|
+
raise RuntimeError(msg)
|
|
738
|
+
|
|
739
|
+
if self.proxies[self.read_trl].has_subscription(): # type: ignore
|
|
740
|
+
self.proxies[self.read_trl].unsubscribe_callback() # type: ignore
|
|
741
|
+
|
|
736
742
|
if callback:
|
|
737
743
|
try:
|
|
738
|
-
assert not self.proxies[self.read_trl].has_subscription() # type: ignore
|
|
739
744
|
self.proxies[self.read_trl].subscribe_callback(callback) # type: ignore
|
|
740
|
-
except AssertionError as ae:
|
|
741
|
-
raise RuntimeError(
|
|
742
|
-
"Cannot set a callback when one" " is already set"
|
|
743
|
-
) from ae
|
|
744
745
|
except RuntimeError as exc:
|
|
745
746
|
raise RuntimeError(
|
|
746
|
-
f"Cannot set callback
|
|
747
|
+
f"Cannot set callback for {self.read_trl}. {exc}"
|
|
747
748
|
) from exc
|
|
748
749
|
|
|
749
|
-
else:
|
|
750
|
-
self.proxies[self.read_trl].unsubscribe_callback() # type: ignore
|
|
751
|
-
|
|
752
750
|
def set_polling(
|
|
753
751
|
self,
|
|
754
752
|
allow_polling: bool = True,
|