ophyd-async 0.5.2__py3-none-any.whl → 0.7.0a1__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 +10 -1
- ophyd_async/__main__.py +12 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +15 -7
- ophyd_async/core/_detector.py +133 -87
- ophyd_async/core/_device.py +13 -15
- ophyd_async/core/_device_save_loader.py +30 -19
- ophyd_async/core/_flyer.py +6 -19
- ophyd_async/core/_hdf_dataset.py +8 -9
- ophyd_async/core/_log.py +3 -1
- ophyd_async/core/_mock_signal_backend.py +11 -9
- ophyd_async/core/_mock_signal_utils.py +8 -5
- ophyd_async/core/_protocol.py +7 -7
- ophyd_async/core/_providers.py +11 -11
- ophyd_async/core/_readable.py +30 -22
- ophyd_async/core/_signal.py +52 -51
- ophyd_async/core/_signal_backend.py +20 -7
- ophyd_async/core/_soft_signal_backend.py +62 -32
- ophyd_async/core/_status.py +7 -9
- ophyd_async/core/_table.py +146 -0
- ophyd_async/core/_utils.py +24 -28
- ophyd_async/epics/adaravis/_aravis_controller.py +20 -19
- ophyd_async/epics/adaravis/_aravis_io.py +2 -1
- ophyd_async/epics/adcore/_core_io.py +2 -0
- ophyd_async/epics/adcore/_core_logic.py +4 -5
- ophyd_async/epics/adcore/_hdf_writer.py +19 -8
- ophyd_async/epics/adcore/_single_trigger.py +1 -1
- ophyd_async/epics/adcore/_utils.py +5 -6
- ophyd_async/epics/adkinetix/_kinetix_controller.py +20 -15
- ophyd_async/epics/adpilatus/_pilatus_controller.py +22 -18
- ophyd_async/epics/adsimdetector/_sim.py +7 -6
- ophyd_async/epics/adsimdetector/_sim_controller.py +22 -17
- ophyd_async/epics/advimba/_vimba_controller.py +22 -17
- ophyd_async/epics/demo/_mover.py +4 -5
- ophyd_async/epics/demo/sensor.db +0 -1
- ophyd_async/epics/eiger/_eiger.py +1 -1
- ophyd_async/epics/eiger/_eiger_controller.py +18 -18
- ophyd_async/epics/eiger/_odin_io.py +6 -5
- ophyd_async/epics/motor.py +8 -10
- ophyd_async/epics/pvi/_pvi.py +30 -33
- ophyd_async/epics/signal/_aioca.py +55 -25
- ophyd_async/epics/signal/_common.py +3 -10
- ophyd_async/epics/signal/_epics_transport.py +11 -8
- ophyd_async/epics/signal/_p4p.py +79 -30
- ophyd_async/epics/signal/_signal.py +6 -8
- ophyd_async/fastcs/panda/__init__.py +0 -6
- ophyd_async/fastcs/panda/_control.py +16 -17
- ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
- ophyd_async/fastcs/panda/_table.py +77 -138
- ophyd_async/fastcs/panda/_trigger.py +4 -5
- ophyd_async/fastcs/panda/_utils.py +3 -2
- ophyd_async/fastcs/panda/_writer.py +28 -13
- ophyd_async/plan_stubs/_fly.py +15 -17
- ophyd_async/plan_stubs/_nd_attributes.py +12 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +27 -21
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
- ophyd_async/sim/demo/_sim_motor.py +2 -1
- ophyd_async/tango/__init__.py +45 -0
- ophyd_async/tango/base_devices/__init__.py +4 -0
- ophyd_async/tango/base_devices/_base_device.py +225 -0
- ophyd_async/tango/base_devices/_tango_readable.py +33 -0
- ophyd_async/tango/demo/__init__.py +12 -0
- ophyd_async/tango/demo/_counter.py +37 -0
- ophyd_async/tango/demo/_detector.py +42 -0
- ophyd_async/tango/demo/_mover.py +77 -0
- ophyd_async/tango/demo/_tango/__init__.py +3 -0
- ophyd_async/tango/demo/_tango/_servers.py +108 -0
- ophyd_async/tango/signal/__init__.py +39 -0
- ophyd_async/tango/signal/_signal.py +223 -0
- ophyd_async/tango/signal/_tango_transport.py +764 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/METADATA +50 -45
- ophyd_async-0.7.0a1.dist-info/RECORD +108 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/WHEEL +1 -1
- ophyd_async-0.5.2.dist-info/RECORD +0 -95
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/LICENSE +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from typing import Optional
|
|
3
2
|
|
|
4
|
-
from ophyd_async.core import
|
|
3
|
+
from ophyd_async.core import DetectorController, PathProvider
|
|
4
|
+
from ophyd_async.core._detector import TriggerInfo
|
|
5
5
|
|
|
6
6
|
from ._pattern_generator import PatternGenerator
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class PatternDetectorController(
|
|
9
|
+
class PatternDetectorController(DetectorController):
|
|
10
10
|
def __init__(
|
|
11
11
|
self,
|
|
12
12
|
pattern_generator: PatternGenerator,
|
|
@@ -14,36 +14,42 @@ class PatternDetectorController(DetectorControl):
|
|
|
14
14
|
exposure: float = 0.1,
|
|
15
15
|
) -> None:
|
|
16
16
|
self.pattern_generator: PatternGenerator = pattern_generator
|
|
17
|
-
if exposure is None:
|
|
18
|
-
exposure = 0.1
|
|
19
17
|
self.pattern_generator.set_exposure(exposure)
|
|
20
18
|
self.path_provider: PathProvider = path_provider
|
|
21
|
-
self.task:
|
|
19
|
+
self.task: asyncio.Task | None = None
|
|
22
20
|
super().__init__()
|
|
23
21
|
|
|
24
|
-
async def
|
|
25
|
-
self
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if exposure is None:
|
|
31
|
-
exposure = 0.1
|
|
32
|
-
period: float = exposure + self.get_deadtime(exposure)
|
|
33
|
-
task = asyncio.create_task(
|
|
34
|
-
self._coroutine_for_image_writing(exposure, period, num)
|
|
22
|
+
async def prepare(self, trigger_info: TriggerInfo):
|
|
23
|
+
self._trigger_info = trigger_info
|
|
24
|
+
if self._trigger_info.livetime is None:
|
|
25
|
+
self._trigger_info.livetime = 0.01
|
|
26
|
+
self.period: float = self._trigger_info.livetime + self.get_deadtime(
|
|
27
|
+
trigger_info.livetime
|
|
35
28
|
)
|
|
36
|
-
self.task = task
|
|
37
|
-
return AsyncStatus(task)
|
|
38
29
|
|
|
39
|
-
async def
|
|
30
|
+
async def arm(self):
|
|
31
|
+
assert self._trigger_info.livetime
|
|
32
|
+
assert self.period
|
|
33
|
+
self.task = asyncio.create_task(
|
|
34
|
+
self._coroutine_for_image_writing(
|
|
35
|
+
self._trigger_info.livetime,
|
|
36
|
+
self.period,
|
|
37
|
+
self._trigger_info.total_number_of_triggers,
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
async def wait_for_idle(self):
|
|
40
42
|
if self.task:
|
|
43
|
+
await self.task
|
|
44
|
+
|
|
45
|
+
async def disarm(self):
|
|
46
|
+
if self.task and not self.task.done():
|
|
41
47
|
self.task.cancel()
|
|
42
48
|
try:
|
|
43
49
|
await self.task
|
|
44
50
|
except asyncio.CancelledError:
|
|
45
51
|
pass
|
|
46
|
-
|
|
52
|
+
self.task = None
|
|
47
53
|
|
|
48
54
|
def get_deadtime(self, exposure: float | None) -> float:
|
|
49
55
|
return 0.001
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from event_model import DataKey
|
|
4
4
|
|
|
5
|
-
from ophyd_async.core import DetectorWriter, NameProvider, PathProvider
|
|
5
|
+
from ophyd_async.core import DEFAULT_TIMEOUT, DetectorWriter, NameProvider, PathProvider
|
|
6
6
|
|
|
7
7
|
from ._pattern_generator import PatternGenerator
|
|
8
8
|
|
|
@@ -20,7 +20,7 @@ class PatternDetectorWriter(DetectorWriter):
|
|
|
20
20
|
self.path_provider = path_provider
|
|
21
21
|
self.name_provider = name_provider
|
|
22
22
|
|
|
23
|
-
async def open(self, multiplier: int = 1) ->
|
|
23
|
+
async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
|
|
24
24
|
return await self.pattern_generator.open_file(
|
|
25
25
|
self.path_provider, self.name_provider(), multiplier
|
|
26
26
|
)
|
|
@@ -31,8 +31,11 @@ class PatternDetectorWriter(DetectorWriter):
|
|
|
31
31
|
def collect_stream_docs(self, indices_written: int) -> AsyncIterator:
|
|
32
32
|
return self.pattern_generator.collect_stream_docs(indices_written)
|
|
33
33
|
|
|
34
|
-
def observe_indices_written(
|
|
35
|
-
|
|
34
|
+
async def observe_indices_written(
|
|
35
|
+
self, timeout=DEFAULT_TIMEOUT
|
|
36
|
+
) -> AsyncGenerator[int, None]:
|
|
37
|
+
async for index in self.pattern_generator.observe_indices_written(timeout):
|
|
38
|
+
yield index
|
|
36
39
|
|
|
37
40
|
async def get_indices_written(self) -> int:
|
|
38
41
|
return self.pattern_generator.image_counter
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
|
1
2
|
from pathlib import Path
|
|
2
|
-
from typing import AsyncGenerator, AsyncIterator, Dict, Optional
|
|
3
3
|
|
|
4
4
|
import h5py
|
|
5
5
|
import numpy as np
|
|
6
|
-
from bluesky.protocols import
|
|
6
|
+
from bluesky.protocols import StreamAsset
|
|
7
|
+
from event_model import DataKey
|
|
7
8
|
|
|
8
9
|
from ophyd_async.core import (
|
|
9
10
|
DEFAULT_TIMEOUT,
|
|
@@ -60,19 +61,22 @@ class PatternGenerator:
|
|
|
60
61
|
generate_gaussian_blob(width=detector_width, height=detector_height)
|
|
61
62
|
* MAX_UINT8_VALUE
|
|
62
63
|
)
|
|
63
|
-
self._hdf_stream_provider:
|
|
64
|
-
self._handle_for_h5_file:
|
|
65
|
-
self.target_path:
|
|
64
|
+
self._hdf_stream_provider: HDFFile | None = None
|
|
65
|
+
self._handle_for_h5_file: h5py.File | None = None
|
|
66
|
+
self.target_path: Path | None = None
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
def write_data_to_dataset(self, path: str, data_shape: tuple[int, ...], data):
|
|
69
|
+
"""Write data to named dataset, resizing to fit and flushing after."""
|
|
68
70
|
assert self._handle_for_h5_file, "no file has been opened!"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
74
|
-
|
|
71
|
+
dset = self._handle_for_h5_file[path]
|
|
72
|
+
assert isinstance(
|
|
73
|
+
dset, h5py.Dataset
|
|
74
|
+
), f"Expected {path} to be dataset, got {dset}"
|
|
75
|
+
dset.resize((self.image_counter + 1,) + data_shape)
|
|
76
|
+
dset[self.image_counter] = data
|
|
77
|
+
dset.flush()
|
|
75
78
|
|
|
79
|
+
async def write_image_to_file(self) -> None:
|
|
76
80
|
# generate the simulated data
|
|
77
81
|
intensity: float = generate_interesting_pattern(self.x, self.y)
|
|
78
82
|
detector_data = (
|
|
@@ -82,14 +86,9 @@ class PatternGenerator:
|
|
|
82
86
|
/ self.saturation_exposure_time
|
|
83
87
|
).astype(np.uint8)
|
|
84
88
|
|
|
85
|
-
#
|
|
86
|
-
self.
|
|
87
|
-
|
|
88
|
-
self._handle_for_h5_file[SUM_PATH][self.image_counter] = sum
|
|
89
|
-
|
|
90
|
-
# save metadata - so that it's discoverable
|
|
91
|
-
self._handle_for_h5_file[DATA_PATH].flush()
|
|
92
|
-
self._handle_for_h5_file[SUM_PATH].flush()
|
|
89
|
+
# Write the data and sum
|
|
90
|
+
self.write_data_to_dataset(DATA_PATH, (self.height, self.width), detector_data)
|
|
91
|
+
self.write_data_to_dataset(SUM_PATH, (), np.sum(detector_data))
|
|
93
92
|
|
|
94
93
|
# counter increment is last
|
|
95
94
|
# as only at this point the new data is visible from the outside
|
|
@@ -107,7 +106,7 @@ class PatternGenerator:
|
|
|
107
106
|
|
|
108
107
|
async def open_file(
|
|
109
108
|
self, path_provider: PathProvider, name: str, multiplier: int = 1
|
|
110
|
-
) ->
|
|
109
|
+
) -> dict[str, DataKey]:
|
|
111
110
|
await self.counter_signal.connect()
|
|
112
111
|
|
|
113
112
|
self.target_path = self._get_new_path(path_provider)
|
|
@@ -156,7 +155,7 @@ class PatternGenerator:
|
|
|
156
155
|
describe = {
|
|
157
156
|
ds.data_key: DataKey(
|
|
158
157
|
source="sim://pattern-generator-hdf-file",
|
|
159
|
-
shape=outer_shape +
|
|
158
|
+
shape=list(outer_shape) + list(ds.shape),
|
|
160
159
|
dtype="array" if ds.shape else "number",
|
|
161
160
|
external="STREAM:",
|
|
162
161
|
)
|
|
@@ -199,7 +198,6 @@ class PatternGenerator:
|
|
|
199
198
|
def close(self) -> None:
|
|
200
199
|
if self._handle_for_h5_file:
|
|
201
200
|
self._handle_for_h5_file.close()
|
|
202
|
-
print("file closed")
|
|
203
201
|
self._handle_for_h5_file = None
|
|
204
202
|
|
|
205
203
|
async def observe_indices_written(
|
|
@@ -63,10 +63,11 @@ class SimMotor(StandardReadable, Movable, Stoppable):
|
|
|
63
63
|
await asyncio.sleep(0.1)
|
|
64
64
|
|
|
65
65
|
@WatchableAsyncStatus.wrap
|
|
66
|
-
async def set(self,
|
|
66
|
+
async def set(self, value: float):
|
|
67
67
|
"""
|
|
68
68
|
Asynchronously move the motor to a new position.
|
|
69
69
|
"""
|
|
70
|
+
new_position = value
|
|
70
71
|
# Make sure any existing move tasks are stopped
|
|
71
72
|
await self.stop()
|
|
72
73
|
old_position, units, velocity = await asyncio.gather(
|
ophyd_async/tango/__init__.py
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from .base_devices import (
|
|
2
|
+
TangoDevice,
|
|
3
|
+
TangoReadable,
|
|
4
|
+
tango_polling,
|
|
5
|
+
)
|
|
6
|
+
from .signal import (
|
|
7
|
+
AttributeProxy,
|
|
8
|
+
CommandProxy,
|
|
9
|
+
TangoSignalBackend,
|
|
10
|
+
__tango_signal_auto,
|
|
11
|
+
ensure_proper_executor,
|
|
12
|
+
get_dtype_extended,
|
|
13
|
+
get_python_type,
|
|
14
|
+
get_tango_trl,
|
|
15
|
+
get_trl_descriptor,
|
|
16
|
+
infer_python_type,
|
|
17
|
+
infer_signal_character,
|
|
18
|
+
make_backend,
|
|
19
|
+
tango_signal_r,
|
|
20
|
+
tango_signal_rw,
|
|
21
|
+
tango_signal_w,
|
|
22
|
+
tango_signal_x,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"TangoDevice",
|
|
27
|
+
"TangoReadable",
|
|
28
|
+
"tango_polling",
|
|
29
|
+
"TangoSignalBackend",
|
|
30
|
+
"get_python_type",
|
|
31
|
+
"get_dtype_extended",
|
|
32
|
+
"get_trl_descriptor",
|
|
33
|
+
"get_tango_trl",
|
|
34
|
+
"infer_python_type",
|
|
35
|
+
"infer_signal_character",
|
|
36
|
+
"make_backend",
|
|
37
|
+
"AttributeProxy",
|
|
38
|
+
"CommandProxy",
|
|
39
|
+
"ensure_proper_executor",
|
|
40
|
+
"__tango_signal_auto",
|
|
41
|
+
"tango_signal_r",
|
|
42
|
+
"tango_signal_rw",
|
|
43
|
+
"tango_signal_w",
|
|
44
|
+
"tango_signal_x",
|
|
45
|
+
]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import (
|
|
4
|
+
TypeVar,
|
|
5
|
+
get_args,
|
|
6
|
+
get_origin,
|
|
7
|
+
get_type_hints,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from ophyd_async.core import (
|
|
11
|
+
DEFAULT_TIMEOUT,
|
|
12
|
+
Device,
|
|
13
|
+
Signal,
|
|
14
|
+
)
|
|
15
|
+
from ophyd_async.tango.signal import (
|
|
16
|
+
TangoSignalBackend,
|
|
17
|
+
__tango_signal_auto,
|
|
18
|
+
make_backend,
|
|
19
|
+
)
|
|
20
|
+
from tango import DeviceProxy as DeviceProxy
|
|
21
|
+
from tango.asyncio import DeviceProxy as AsyncDeviceProxy
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TangoDevice(Device):
|
|
27
|
+
"""
|
|
28
|
+
General class for TangoDevices. Extends Device to provide attributes for Tango
|
|
29
|
+
devices.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
trl: str
|
|
34
|
+
Tango resource locator, typically of the device server.
|
|
35
|
+
device_proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]]
|
|
36
|
+
Asynchronous or synchronous DeviceProxy object for the device. If not provided,
|
|
37
|
+
an asynchronous DeviceProxy object will be created using the trl and awaited
|
|
38
|
+
when the device is connected.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
trl: str = ""
|
|
42
|
+
proxy: DeviceProxy | None = None
|
|
43
|
+
_polling: tuple[bool, float, float | None, float | None] = (False, 0.1, None, 0.1)
|
|
44
|
+
_signal_polling: dict[str, tuple[bool, float, float, float]] = {}
|
|
45
|
+
_poll_only_annotated_signals: bool = True
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
trl: str | None = None,
|
|
50
|
+
device_proxy: DeviceProxy | None = None,
|
|
51
|
+
name: str = "",
|
|
52
|
+
) -> None:
|
|
53
|
+
self.trl = trl if trl else ""
|
|
54
|
+
self.proxy = device_proxy
|
|
55
|
+
tango_create_children_from_annotations(self)
|
|
56
|
+
super().__init__(name=name)
|
|
57
|
+
|
|
58
|
+
def set_trl(self, trl: str):
|
|
59
|
+
"""Set the Tango resource locator."""
|
|
60
|
+
if not isinstance(trl, str):
|
|
61
|
+
raise ValueError("TRL must be a string.")
|
|
62
|
+
self.trl = trl
|
|
63
|
+
|
|
64
|
+
async def connect(
|
|
65
|
+
self,
|
|
66
|
+
mock: bool = False,
|
|
67
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
68
|
+
force_reconnect: bool = False,
|
|
69
|
+
):
|
|
70
|
+
if self.trl and self.proxy is None:
|
|
71
|
+
self.proxy = await AsyncDeviceProxy(self.trl)
|
|
72
|
+
elif self.proxy and not self.trl:
|
|
73
|
+
self.trl = self.proxy.name()
|
|
74
|
+
|
|
75
|
+
# Set the trl of the signal backends
|
|
76
|
+
for child in self.children():
|
|
77
|
+
if isinstance(child[1], Signal):
|
|
78
|
+
if isinstance(child[1]._backend, TangoSignalBackend): # noqa: SLF001
|
|
79
|
+
resource_name = child[0].lstrip("_")
|
|
80
|
+
read_trl = f"{self.trl}/{resource_name}"
|
|
81
|
+
child[1]._backend.set_trl(read_trl, read_trl) # noqa: SLF001
|
|
82
|
+
|
|
83
|
+
if self.proxy is not None:
|
|
84
|
+
self.register_signals()
|
|
85
|
+
await _fill_proxy_entries(self)
|
|
86
|
+
|
|
87
|
+
# set_name should be called again to propagate the new signal names
|
|
88
|
+
self.set_name(self.name)
|
|
89
|
+
|
|
90
|
+
# Set the polling configuration
|
|
91
|
+
if self._polling[0]:
|
|
92
|
+
for child in self.children():
|
|
93
|
+
child_type = type(child[1])
|
|
94
|
+
if issubclass(child_type, Signal):
|
|
95
|
+
if isinstance(child[1]._backend, TangoSignalBackend): # noqa: SLF001 # type: ignore
|
|
96
|
+
child[1]._backend.set_polling(*self._polling) # noqa: SLF001 # type: ignore
|
|
97
|
+
child[1]._backend.allow_events(False) # noqa: SLF001 # type: ignore
|
|
98
|
+
if self._signal_polling:
|
|
99
|
+
for signal_name, polling in self._signal_polling.items():
|
|
100
|
+
if hasattr(self, signal_name):
|
|
101
|
+
attr = getattr(self, signal_name)
|
|
102
|
+
if isinstance(attr._backend, TangoSignalBackend): # noqa: SLF001
|
|
103
|
+
attr._backend.set_polling(*polling) # noqa: SLF001
|
|
104
|
+
attr._backend.allow_events(False) # noqa: SLF001
|
|
105
|
+
|
|
106
|
+
await super().connect(mock=mock, timeout=timeout)
|
|
107
|
+
|
|
108
|
+
# Users can override this method to register new signals
|
|
109
|
+
def register_signals(self):
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def tango_polling(
|
|
114
|
+
polling: tuple[float, float, float]
|
|
115
|
+
| dict[str, tuple[float, float, float]]
|
|
116
|
+
| None = None,
|
|
117
|
+
signal_polling: dict[str, tuple[float, float, float]] | None = None,
|
|
118
|
+
):
|
|
119
|
+
"""
|
|
120
|
+
Class decorator to configure polling for Tango devices.
|
|
121
|
+
|
|
122
|
+
This decorator allows for the configuration of both device-level and signal-level
|
|
123
|
+
polling for Tango devices. Polling is useful for device servers that do not support
|
|
124
|
+
event-driven updates.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
polling : Optional[Union[Tuple[float, float, float],
|
|
129
|
+
Dict[str, Tuple[float, float, float]]]], optional
|
|
130
|
+
Device-level polling configuration as a tuple of three floats representing the
|
|
131
|
+
polling interval, polling timeout, and polling delay. Alternatively,
|
|
132
|
+
a dictionary can be provided to specify signal-level polling configurations
|
|
133
|
+
directly.
|
|
134
|
+
signal_polling : Optional[Dict[str, Tuple[float, float, float]]], optional
|
|
135
|
+
Signal-level polling configuration as a dictionary where keys are signal names
|
|
136
|
+
and values are tuples of three floats representing the polling interval, polling
|
|
137
|
+
timeout, and polling delay.
|
|
138
|
+
"""
|
|
139
|
+
if isinstance(polling, dict):
|
|
140
|
+
signal_polling = polling
|
|
141
|
+
polling = None
|
|
142
|
+
|
|
143
|
+
def decorator(cls):
|
|
144
|
+
if polling is not None:
|
|
145
|
+
cls._polling = (True, *polling)
|
|
146
|
+
if signal_polling is not None:
|
|
147
|
+
cls._signal_polling = {k: (True, *v) for k, v in signal_polling.items()}
|
|
148
|
+
return cls
|
|
149
|
+
|
|
150
|
+
return decorator
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def tango_create_children_from_annotations(
|
|
154
|
+
device: TangoDevice, included_optional_fields: tuple[str, ...] = ()
|
|
155
|
+
):
|
|
156
|
+
"""Initialize blocks at __init__ of `device`."""
|
|
157
|
+
for name, device_type in get_type_hints(type(device)).items():
|
|
158
|
+
if name in ("_name", "parent"):
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
# device_type, is_optional = _strip_union(device_type)
|
|
162
|
+
# if is_optional and name not in included_optional_fields:
|
|
163
|
+
# continue
|
|
164
|
+
#
|
|
165
|
+
# is_device_vector, device_type = _strip_device_vector(device_type)
|
|
166
|
+
# if is_device_vector:
|
|
167
|
+
# n_device_vector = DeviceVector()
|
|
168
|
+
# setattr(device, name, n_device_vector)
|
|
169
|
+
|
|
170
|
+
# else:
|
|
171
|
+
origin = get_origin(device_type)
|
|
172
|
+
origin = origin if origin else device_type
|
|
173
|
+
|
|
174
|
+
if issubclass(origin, Signal):
|
|
175
|
+
type_args = get_args(device_type)
|
|
176
|
+
datatype = type_args[0] if type_args else None
|
|
177
|
+
backend = make_backend(datatype=datatype, device_proxy=device.proxy)
|
|
178
|
+
setattr(device, name, origin(name=name, backend=backend))
|
|
179
|
+
|
|
180
|
+
elif issubclass(origin, Device) or isinstance(origin, Device):
|
|
181
|
+
assert callable(origin), f"{origin} is not callable."
|
|
182
|
+
setattr(device, name, origin())
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def _fill_proxy_entries(device: TangoDevice):
|
|
186
|
+
if device.proxy is None:
|
|
187
|
+
raise RuntimeError(f"Device proxy is not connected for {device.name}")
|
|
188
|
+
proxy_trl = device.trl
|
|
189
|
+
children = [name.lstrip("_") for name, _ in device.children()]
|
|
190
|
+
proxy_attributes = list(device.proxy.get_attribute_list())
|
|
191
|
+
proxy_commands = list(device.proxy.get_command_list())
|
|
192
|
+
combined = proxy_attributes + proxy_commands
|
|
193
|
+
|
|
194
|
+
for name in combined:
|
|
195
|
+
if name not in children:
|
|
196
|
+
full_trl = f"{proxy_trl}/{name}"
|
|
197
|
+
try:
|
|
198
|
+
auto_signal = await __tango_signal_auto(
|
|
199
|
+
trl=full_trl, device_proxy=device.proxy
|
|
200
|
+
)
|
|
201
|
+
setattr(device, name, auto_signal)
|
|
202
|
+
except RuntimeError as e:
|
|
203
|
+
if "Commands with different in and out dtypes" in str(e):
|
|
204
|
+
print(
|
|
205
|
+
f"Skipping {name}. Commands with different in and out dtypes"
|
|
206
|
+
f" are not supported."
|
|
207
|
+
)
|
|
208
|
+
continue
|
|
209
|
+
raise e
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# def _strip_union(field: T | T) -> tuple[T, bool]:
|
|
213
|
+
# if get_origin(field) is Union:
|
|
214
|
+
# args = get_args(field)
|
|
215
|
+
# is_optional = type(None) in args
|
|
216
|
+
# for arg in args:
|
|
217
|
+
# if arg is not type(None):
|
|
218
|
+
# return arg, is_optional
|
|
219
|
+
# return field, False
|
|
220
|
+
#
|
|
221
|
+
#
|
|
222
|
+
# def _strip_device_vector(field: type[Device]) -> tuple[bool, type[Device]]:
|
|
223
|
+
# if get_origin(field) is DeviceVector:
|
|
224
|
+
# return True, get_args(field)[0]
|
|
225
|
+
# return False, field
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
StandardReadable,
|
|
5
|
+
)
|
|
6
|
+
from ophyd_async.tango.base_devices._base_device import TangoDevice
|
|
7
|
+
from tango import DeviceProxy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TangoReadable(TangoDevice, StandardReadable):
|
|
11
|
+
"""
|
|
12
|
+
General class for readable TangoDevices. Extends StandardReadable to provide
|
|
13
|
+
attributes for Tango devices.
|
|
14
|
+
|
|
15
|
+
Usage: to proper signals mount should be awaited:
|
|
16
|
+
new_device = await TangoDevice(<tango_device>)
|
|
17
|
+
|
|
18
|
+
Attributes
|
|
19
|
+
----------
|
|
20
|
+
trl : str
|
|
21
|
+
Tango resource locator, typically of the device server.
|
|
22
|
+
proxy : AsyncDeviceProxy
|
|
23
|
+
AsyncDeviceProxy object for the device. This is created when the
|
|
24
|
+
device is connected.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
trl: str | None = None,
|
|
30
|
+
device_proxy: DeviceProxy | None = None,
|
|
31
|
+
name: str = "",
|
|
32
|
+
) -> None:
|
|
33
|
+
TangoDevice.__init__(self, trl, device_proxy=device_proxy, name=name)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from ._counter import TangoCounter
|
|
2
|
+
from ._detector import TangoDetector
|
|
3
|
+
from ._mover import TangoMover
|
|
4
|
+
from ._tango import DemoCounter, DemoMover
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"DemoCounter",
|
|
8
|
+
"DemoMover",
|
|
9
|
+
"TangoCounter",
|
|
10
|
+
"TangoMover",
|
|
11
|
+
"TangoDetector",
|
|
12
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from ophyd_async.core import (
|
|
2
|
+
DEFAULT_TIMEOUT,
|
|
3
|
+
AsyncStatus,
|
|
4
|
+
ConfigSignal,
|
|
5
|
+
HintedSignal,
|
|
6
|
+
SignalR,
|
|
7
|
+
SignalRW,
|
|
8
|
+
SignalX,
|
|
9
|
+
)
|
|
10
|
+
from ophyd_async.tango import TangoReadable, tango_polling
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Enable device level polling, useful for servers that do not support events
|
|
14
|
+
# Polling for individual signal can be enabled with a dict
|
|
15
|
+
@tango_polling({"counts": (1.0, 0.1, 0.1), "sample_time": (0.1, 0.1, 0.1)})
|
|
16
|
+
class TangoCounter(TangoReadable):
|
|
17
|
+
# Enter the name and type of the signals you want to use
|
|
18
|
+
# If type is None or Signal, the type will be inferred from the Tango device
|
|
19
|
+
counts: SignalR[int]
|
|
20
|
+
sample_time: SignalRW[float]
|
|
21
|
+
start: SignalX
|
|
22
|
+
_reset: SignalX
|
|
23
|
+
|
|
24
|
+
def __init__(self, trl: str | None = "", name=""):
|
|
25
|
+
super().__init__(trl, name=name)
|
|
26
|
+
self.add_readables([self.counts], HintedSignal)
|
|
27
|
+
self.add_readables([self.sample_time], ConfigSignal)
|
|
28
|
+
|
|
29
|
+
@AsyncStatus.wrap
|
|
30
|
+
async def trigger(self) -> None:
|
|
31
|
+
sample_time = await self.sample_time.get_value()
|
|
32
|
+
timeout = sample_time + DEFAULT_TIMEOUT
|
|
33
|
+
await self.start.trigger(wait=True, timeout=timeout)
|
|
34
|
+
|
|
35
|
+
@AsyncStatus.wrap
|
|
36
|
+
async def reset(self) -> None:
|
|
37
|
+
await self._reset.trigger(wait=True, timeout=DEFAULT_TIMEOUT)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
AsyncStatus,
|
|
5
|
+
DeviceVector,
|
|
6
|
+
StandardReadable,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from ._counter import TangoCounter
|
|
10
|
+
from ._mover import TangoMover
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TangoDetector(StandardReadable):
|
|
14
|
+
def __init__(self, mover_trl: str, counter_trls: list[str], name=""):
|
|
15
|
+
# A detector device may be composed of tango sub-devices
|
|
16
|
+
self.mover = TangoMover(mover_trl)
|
|
17
|
+
self.counters = DeviceVector(
|
|
18
|
+
{i + 1: TangoCounter(c_trl) for i, c_trl in enumerate(counter_trls)}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Define the readables for TangoDetector
|
|
22
|
+
# DeviceVectors are incompatible with AsyncReadable. Ignore until fixed.
|
|
23
|
+
self.add_readables([self.counters, self.mover]) # type: ignore
|
|
24
|
+
|
|
25
|
+
super().__init__(name=name)
|
|
26
|
+
|
|
27
|
+
def set(self, value):
|
|
28
|
+
return self.mover.set(value)
|
|
29
|
+
|
|
30
|
+
def stop(self, success: bool = True) -> AsyncStatus:
|
|
31
|
+
return self.mover.stop(success)
|
|
32
|
+
|
|
33
|
+
@AsyncStatus.wrap
|
|
34
|
+
async def trigger(self):
|
|
35
|
+
statuses = []
|
|
36
|
+
for counter in self.counters.values():
|
|
37
|
+
statuses.append(counter.reset())
|
|
38
|
+
await asyncio.gather(*statuses)
|
|
39
|
+
statuses.clear()
|
|
40
|
+
for counter in self.counters.values():
|
|
41
|
+
statuses.append(counter.trigger())
|
|
42
|
+
await asyncio.gather(*statuses)
|