From 1b18cd14cb7be0a6d9e285b5f5af023f41a9ec48 Mon Sep 17 00:00:00 2001 From: matveyev Date: Mon, 16 Oct 2023 12:25:14 +0200 Subject: [PATCH 001/141] first try. Made signals pv for attributes --- src/ophyd_async/core/utils.py | 1 + src/ophyd_async/tango/__init__.py | 8 + src/ophyd_async/tango/_backend/__init__.py | 1 + .../tango/_backend/_tango_transport.py | 287 ++++++++++++++++++ src/ophyd_async/tango/signal/__init__.py | 8 + src/ophyd_async/tango/signal/signal.py | 93 ++++++ tests/tango/test_signals.py | 270 ++++++++++++++++ 7 files changed, 668 insertions(+) create mode 100644 src/ophyd_async/tango/__init__.py create mode 100644 src/ophyd_async/tango/_backend/__init__.py create mode 100644 src/ophyd_async/tango/_backend/_tango_transport.py create mode 100644 src/ophyd_async/tango/signal/__init__.py create mode 100644 src/ophyd_async/tango/signal/signal.py create mode 100644 tests/tango/test_signals.py diff --git a/src/ophyd_async/core/utils.py b/src/ophyd_async/core/utils.py index 42a4b5b7d4..ac461f1820 100644 --- a/src/ophyd_async/core/utils.py +++ b/src/ophyd_async/core/utils.py @@ -14,6 +14,7 @@ Union, ) +from tango import EventData import numpy as np from bluesky.protocols import Reading diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py new file mode 100644 index 0000000000..412c0d9291 --- /dev/null +++ b/src/ophyd_async/tango/__init__.py @@ -0,0 +1,8 @@ +from ophyd_async.tango.signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x + +__all__ = [ + "tango_signal_r", + "tango_signal_rw", + "tango_signal_w", + "tango_signal_x", +] diff --git a/src/ophyd_async/tango/_backend/__init__.py b/src/ophyd_async/tango/_backend/__init__.py new file mode 100644 index 0000000000..2c3b266408 --- /dev/null +++ b/src/ophyd_async/tango/_backend/__init__.py @@ -0,0 +1 @@ +from ._tango_transport import TangoTransport \ No newline at end of file diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py new file mode 100644 index 0000000000..66478bc82d --- /dev/null +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -0,0 +1,287 @@ +import asyncio +import numpy as np +from asyncio import CancelledError + +from abc import abstractmethod +from enum import Enum +from typing import Any, Dict, List, Optional, Sequence, Type, Union + +from tango import AttributeConfig, AttrDataFormat, CmdArgType, EventType, GreenMode, DevState +from tango.asyncio import DeviceProxy +from tango.utils import is_int, is_float, is_bool, is_str, is_binary + +from bluesky.protocols import Descriptor, Dtype, Reading + +from ophyd_async.core import ( + NotConnected, + ReadingValueCallback, + SignalBackend, + T, + get_dtype, + get_unique, + wait_for_connection, +) + + +# -------------------------------------------------------------------- +def get_pyton_type(tango_type): + if is_int(tango_type): + return int, "integer" + if is_float(tango_type): + return float, "number" + if is_bool(tango_type): + return bool, "integer" + if is_str(tango_type): + return str, "string" + if is_binary(tango_type): + return list[str], "string" + if tango_type == CmdArgType.DevEnum: + return Enum, "string" + if tango_type == CmdArgType.DevState: + return CmdArgType.DevState, "string" + if tango_type == CmdArgType.DevUChar: + return int, "integer" + if tango_type == CmdArgType.DevVoid: + return None, "string" + raise TypeError("Unknown TangoType") + + +# -------------------------------------------------------------------- +class TangoProxy: + + support_events = False + + def __init__(self, device_proxy: DeviceProxy, name: str): + self._proxy = device_proxy + self._name = name + + # -------------------------------------------------------------------- + @abstractmethod + async def get(self) -> T: + """Get value from PV""" + + # -------------------------------------------------------------------- + @abstractmethod + async def put(self, value: Optional[T], wait: bool=True, timeout: Optional[float]=None) -> None: + """Get value from PV""" + + # -------------------------------------------------------------------- + @abstractmethod + async def get_config(self) -> AttributeConfig: + """Get value from PV""" + + # -------------------------------------------------------------------- + @abstractmethod + async def get_reading(self) -> Reading: + """Get value from PV""" + + # -------------------------------------------------------------------- + def has_subscription(self) -> bool: + """indicates, that this pv already subscribed""" + + # -------------------------------------------------------------------- + @abstractmethod + def subscribe_callback(self, callback: Optional[ReadingValueCallback]): + """subscribe tango CHANGE event to callback""" + + # -------------------------------------------------------------------- + @abstractmethod + def unsubscribe_callback(self, callback: Optional[ReadingValueCallback]): + """delete CHANGE event subscription""" + + +# -------------------------------------------------------------------- +class AttributeProxy(TangoProxy): + + support_events = True + _event_callback = None + _eid = None + + # -------------------------------------------------------------------- + async def get(self) -> T: + attr = await self._proxy.read_attribute(self._name) + return attr.value + + # -------------------------------------------------------------------- + async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None) -> None: + if wait: + if timeout: + id = self._proxy.write_attribute_asynch(self._name, value, green_mode=GreenMode.Synchronous) + await asyncio.sleep(timeout) + self._proxy.write_attribute_reply(id, green_mode=GreenMode.Synchronous) + else: + self._proxy.write_attribute(self._name, value, green_mode=GreenMode.Synchronous) + else: + await self._proxy.write_attribute(self._name, value, wait=wait, timeout=timeout) + + # -------------------------------------------------------------------- + async def get_config(self) -> AttributeConfig: + return await self._proxy.get_attribute_config(self._name) + + # -------------------------------------------------------------------- + async def get_reading(self) -> Reading: + attr = await self._proxy.read_attribute(self._name) + return dict( + value=attr.value, + timestamp=attr.time.totime(), + alarm_severity=attr.quality, + ) + + # -------------------------------------------------------------------- + def has_subscription(self) -> bool: + return bool(self._eid) + + # -------------------------------------------------------------------- + def subscribe_callback(self, callback: Optional[ReadingValueCallback]): + """add user callack to delete CHANGE event subscription""" + self._event_callback = callback + self._eid = self._proxy.subscribe_event(self._name, EventType.CHANGE_EVENT, self._event_processor) + + # -------------------------------------------------------------------- + def unsubscribe_callback(self, eid: int): + self._proxy.unsubscribe_event(self._eid) + self._eid = None + self._event_callback = None + + # -------------------------------------------------------------------- + def _event_processor(self, event): + if not event.err: + value = event.attr_value.value + reading = dict(value=value, + timestamp=event.get_date().totime(), + alarm_severity=event.attr_value.quality) + + self._event_callback(reading, value) + + +# -------------------------------------------------------------------- +async def get_tango_pv(full_trl: str, device_proxy: Optional[DeviceProxy]) -> TangoProxy: + device_trl, pv_name = full_trl.rsplit('/', 1) + device_proxy = device_proxy or await DeviceProxy(device_trl) + if pv_name in device_proxy.get_attribute_list(): + return AttributeProxy(device_proxy, pv_name) + + +# -------------------------------------------------------------------- +def get_dtype_extendet(datatype): + # DevState tango type does not have numpy equivalents + dtype = get_dtype(datatype) + if dtype == np.object_: + if datatype.__args__[1].__args__[0] == DevState: + dtype = CmdArgType.DevState + return dtype + + +# -------------------------------------------------------------------- +def get_descriptor(datatype: Optional[Type], pv: str, attr_config: Dict[str, AttributeConfig]) -> dict: + + pv_dtype, pv_dtype_desc = get_unique({k: get_pyton_type(v.data_type) for k, v in attr_config.items()}, "typeids") + attr_config = list(attr_config.values())[0] + + if attr_config.data_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]: + # This is an array + if datatype: + # Check we wanted an array of this type + dtype = get_dtype_extendet(datatype) + if not dtype: + raise TypeError(f"{pv} has type [{pv_dtype}] not {datatype.__name__}") + if dtype != pv_dtype: + raise TypeError(f"{pv} has type [{pv_dtype}] not [{dtype}]") + + if attr_config.data_format == AttrDataFormat.SPECTRUM: + return dict(source=pv, dtype="array", shape=[attr_config.max_dim_x]) + else: + return dict(source=pv, dtype="array", shape=[attr_config.max_dim_y, attr_config.max_dim_x]) + + else: + if pv_dtype in (Enum, CmdArgType.DevState): + if pv_dtype == Enum: + pv_choices = list(attr_config.enum_labels) + else: + pv_choices = list(DevState.names.keys()) + + if datatype: + if not issubclass(datatype, (Enum, DevState)): + raise TypeError(f"{pv} has type Enum not {datatype.__name__}") + if pv_dtype == Enum: + choices = tuple(v.name for v in datatype) + if set(choices) != set(pv_choices): + raise TypeError(f"{pv} has choices {pv_choices} not {choices}") + return dict(source=pv, dtype="string", shape=[], choices=pv_choices) + else: + if datatype and not issubclass(pv_dtype, datatype): + raise TypeError(f"{pv} has type {pv_dtype.__name__} not {datatype.__name__}") + return dict(source=pv, dtype=pv_dtype_desc, shape=[]) + + +# -------------------------------------------------------------------- +class TangoTransport(SignalBackend[T]): + + def __init__(self, + datatype: Optional[Type[T]], + read_pv: str, + write_pv: str, + device_proxy: Optional[DeviceProxy] = None): + self.datatype = datatype + self.read_pv = read_pv + self.write_pv = write_pv + self.proxies: Dict[str, TangoProxy] = {read_pv: device_proxy, write_pv: device_proxy} + self.initial_readings: Dict[str, Any] = {} + self.pv_configs: Dict[str, AttributeConfig] = {} + self.source = f"{self.read_pv}" + self.descriptor: Descriptor = {} # type: ignore + self.eid: Optional[int] = None + + # -------------------------------------------------------------------- + async def _connect_and_store_initial_value(self, pv): + try: + self.proxies[pv] = await get_tango_pv(pv, self.proxies[pv]) + self.initial_readings[pv] = await self.proxies[pv].get() + self.pv_configs[pv] = await self.proxies[pv].get_config() + except CancelledError: + raise NotConnected(self.source) + + # -------------------------------------------------------------------- + async def connect(self): + if self.read_pv != self.write_pv: + # Different, need to connect both + await wait_for_connection( + read_pv=self._connect_and_store_initial_value(self.read_pv), + write_pv=self._connect_and_store_initial_value(self.write_pv), + ) + else: + # The same, so only need to connect one + await self._connect_and_store_initial_value(self.read_pv) + self.descriptor = get_descriptor(self.datatype, self.read_pv, self.pv_configs) + + # -------------------------------------------------------------------- + async def put(self, write_value: Optional[T], wait=True, timeout=None): + if write_value is None: + write_value = self.initial_readings[self.write_pv]["value"] + + await self.proxies[self.write_pv].put(write_value, wait, timeout) + + # -------------------------------------------------------------------- + async def get_descriptor(self) -> Descriptor: + return self.descriptor + + # -------------------------------------------------------------------- + async def get_reading(self) -> Reading: + return await self.proxies[self.read_pv].get_reading() + + # -------------------------------------------------------------------- + async def get_value(self) -> T: + return await self.proxies[self.write_pv].get() + + # -------------------------------------------------------------------- + def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: + assert self.proxies[self.read_pv].support_events, f"{self.source} does not support events" + + if callback: + assert (not self.proxies[self.read_pv].has_subscription()), "Cannot set a callback when one is already set" + self.eid = self.proxies[self.read_pv].subscribe_callback(callback) + + else: + if self.eid: + self.proxies[self.read_pv].unsubscribe_callback(self.eid) + self.eid = None \ No newline at end of file diff --git a/src/ophyd_async/tango/signal/__init__.py b/src/ophyd_async/tango/signal/__init__.py new file mode 100644 index 0000000000..6ca2a387a1 --- /dev/null +++ b/src/ophyd_async/tango/signal/__init__.py @@ -0,0 +1,8 @@ +from .signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x + +__all__ = [ + "tango_signal_r", + "tango_signal_rw", + "tango_signal_w", + "tango_signal_x", +] diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py new file mode 100644 index 0000000000..2c417453db --- /dev/null +++ b/src/ophyd_async/tango/signal/signal.py @@ -0,0 +1,93 @@ +"""Tango Signals over Pytango""" + +from __future__ import annotations + +from typing import Optional, Tuple, Type +from tango import DeviceProxy + +from ophyd_async.core import ( + SignalBackend, + SignalR, + SignalRW, + SignalW, + SignalX, + T, +) + +from .._backend import TangoTransport + + +def tango_signal_rw(datatype: Type[T], + read_pv: str, + write_pv: Optional[str] = None, + device_proxy: Optional[DeviceProxy] = None + ) -> SignalRW[T]: + """Create a `SignalRW` backed by 1 or 2 EPICS PVs + + Parameters + ---------- + datatype: + Check that the PV is of this type + read_pv: + The PV to read and monitor + write_pv: + If given, use this PV to write to, otherwise use read_pv + device_proxy: + If given, this DeviceProxy will be used + """ + backend = TangoTransport(datatype, read_pv, write_pv or read_pv, device_proxy) + return SignalRW(backend) + + +def tango_signal_r(datatype: Type[T], + read_pv: str, + device_proxy: Optional[DeviceProxy] = None + ) -> SignalR[T]: + """Create a `SignalR` backed by 1 EPICS PV + + Parameters + ---------- + datatype: + Check that the PV is of this type + read_pv: + The PV to read and monitor + device_proxy: + If given, this DeviceProxy will be used + """ + backend = TangoTransport(datatype, read_pv, read_pv, device_proxy) + return SignalR(backend) + + +def tango_signal_w(datatype: Type[T], + write_pv: str, + device_proxy: Optional[DeviceProxy] = None + ) -> SignalW[T]: + """Create a `SignalW` backed by 1 EPICS PVs + + Parameters + ---------- + datatype: + Check that the PV is of this type + write_pv: + The PV to write to + device_proxy: + If given, this DeviceProxy will be used + """ + backend = TangoTransport(datatype, write_pv, write_pv, device_proxy) + return SignalW(backend) + + +def tango_signal_x(write_pv: str, + device_proxy: Optional[DeviceProxy] = None + ) -> SignalX: + """Create a `SignalX` backed by 1 EPICS PVs + + Parameters + ---------- + write_pv: + The PV to write its initial value to on execute + device_proxy: + If given, this DeviceProxy will be used + """ + backend: SignalBackend = TangoTransport(None, write_pv, write_pv, device_proxy) + return SignalX(backend) diff --git a/tests/tango/test_signals.py b/tests/tango/test_signals.py new file mode 100644 index 0000000000..4ba9e7c239 --- /dev/null +++ b/tests/tango/test_signals.py @@ -0,0 +1,270 @@ +import pytest +import asyncio +import time + +import numpy as np +import numpy.typing as npt + +from typing import Any, Optional, Tuple, Type + +from enum import Enum, IntEnum + +from tango import AttrWriteType, AttrDataFormat, DeviceProxy, DevState +from tango.server import Device, attribute +from tango.test_utils import assert_close +from tango.test_context import MultiDeviceTestContext +from tango.asyncio_executor import set_global_executor + +from bluesky.protocols import Reading + +from ophyd_async.core import SignalBackend, T +from ophyd_async.tango._backend import TangoTransport + + +# -------------------------------------------------------------------- +""" +Since TangoTest does not support EchoMode, we create our own Device. + +""" + + +class TestEnum(IntEnum): + A = 0 + B = 1 + + +def get_enum_labels(enum_cls): + if enum_cls == DevState: + return list(enum_cls.names.keys()) + else: + return [member.name for member in enum_cls] + + +TEST_ENUM_CLASS_LABELS = get_enum_labels(TestEnum) + + +TYPES_TO_TEST = ( + ("boolean_scalar", 'DevBoolean', AttrDataFormat.SCALAR, bool, True, False), + ("boolean_spectrum", 'DevBoolean', AttrDataFormat.SPECTRUM, npt.NDArray[np.bool_], [True, False], [False, True]), + ("boolean_image", 'DevBoolean', AttrDataFormat.IMAGE, npt.NDArray[np.bool_], [[True, False], [False, True]], + np.array([[False, True], [True, False]])), + + ("short_scalar", 'DevShort', AttrDataFormat.SCALAR, int, 1, 2), + ("short_spectrum", 'DevShort', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), + ("short_image", 'DevShort', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), + + ("long_scalar", 'DevLong', AttrDataFormat.SCALAR, int, 1, 2), + ("long_spectrum", 'DevLong', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), + ("long_image", 'DevLong', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), + + ("ushort_scalar", 'DevUShort', AttrDataFormat.SCALAR, int, 1, 2), + ("ushort_spectrum", 'DevUShort', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), + ("ushort_image", 'DevUShort', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), + + ("ulong_scalar", 'DevLong', AttrDataFormat.SCALAR, int, 1, 2), + ("ulong_spectrum", 'DevLong', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), + ("ulong_image", 'DevLong', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), + + ("ulong64_scalar", 'DevULong64', AttrDataFormat.SCALAR, int, 1, 2), + ("ulong64_spectrum", 'DevULong64', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), + ("ulong64_image", 'DevULong64', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), + + ("long64_scalar", 'DevLong64', AttrDataFormat.SCALAR, int, 1, 2), + ("long64_spectrum", 'DevLong64', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), + ("long64_image", 'DevLong64', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), + + ("char_scalar", 'DevUChar', AttrDataFormat.SCALAR, int, 1, 2), + ("char_spectrum", 'DevUChar', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), + ("char_image", 'DevUChar', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), + + ("float_scalar", 'DevFloat', AttrDataFormat.SCALAR, float, 1.1, 2.2), + ("float_spectrum", 'DevFloat', AttrDataFormat.SPECTRUM, npt.NDArray[np.float_], [1.1, 2.2], [3.3, 4.4]), + ("float_image", 'DevFloat', AttrDataFormat.IMAGE, npt.NDArray[np.float_], [[1.1, 2.2], [3.3, 4.4]], + [[5.5, 6.6], [7.7, 8.8]]), + + ("double_scalar", 'DevDouble', AttrDataFormat.SCALAR, float, 1.1, 2.2), + ("double_spectrum", 'DevDouble', AttrDataFormat.SPECTRUM, npt.NDArray[np.float_], [1.1, 2.2], [3.3, 4.4]), + ("double_image", 'DevDouble', AttrDataFormat.IMAGE, npt.NDArray[np.float_], [[1.1, 2.2], [3.3, 4.4]], + [[5.5, 6.6], [7.7, 8.8]]), + + ("string_scalar", 'DevString', AttrDataFormat.SCALAR, str, "aaa", "bbb"), + ("string_spectrum", 'DevString', AttrDataFormat.SPECTRUM, npt.NDArray[str], ["aaa", "bbb"], ["ccc", "ddd"]), + ("string_image", 'DevString', AttrDataFormat.IMAGE, npt.NDArray[str], [["aaa", "bbb"], ["ccc", "ddd"]], + [["eee", "fff"], ["ggg", "hhh"]]), + + ("state_scalar", 'DevState', AttrDataFormat.SCALAR, DevState, DevState.ON, DevState.OFF), + ("state_spectrum", 'DevState', AttrDataFormat.SPECTRUM, npt.NDArray[DevState], [DevState.ON, DevState.OFF], + [DevState.OFF, DevState.ON]), + ("state_image", 'DevState', AttrDataFormat.IMAGE, npt.NDArray[DevState], + [[DevState.ON, DevState.OFF], [DevState.OFF, DevState.ON]], + [[DevState.OFF, DevState.ON], [DevState.ON, DevState.OFF]]), + + ("enum_scalar", 'DevEnum', AttrDataFormat.SCALAR, TestEnum, TestEnum.A, TestEnum.B), + ("enum_spectrum", 'DevEnum', AttrDataFormat.SPECTRUM, npt.NDArray[TestEnum], [TestEnum.A, TestEnum.B], + [TestEnum.B, TestEnum.A]), + ("enum_image", 'DevEnum', AttrDataFormat.IMAGE, npt.NDArray[TestEnum], + [[TestEnum.A, TestEnum.B], [TestEnum.B, TestEnum.A]], + [[TestEnum.B, TestEnum.A], [TestEnum.A, TestEnum.B]]), + + # ('DevEncoded': tango._tango.CmdArgType.DevEncoded, + ) + + +# -------------------------------------------------------------------- +# Echo device +# -------------------------------------------------------------------- +class EchoDevice(Device): + attr_values = {} + + def initialize_dynamic_attributes(self): + for name, typ, form, _, _, _ in TYPES_TO_TEST: + attr = attribute( + name=name, + dtype=typ, + dformat=form, + access=AttrWriteType.READ_WRITE, + fget=self.read, + fset=self.write, + max_dim_x=2, + max_dim_y=2, + enum_labels=TEST_ENUM_CLASS_LABELS + ) + self.add_attribute(attr) + self.set_change_event(name, True, False) + + def read(self, attr): + attr.set_value(self.attr_values[attr.get_name()]) + + def write(self, attr): + new_value = attr.get_write_value() + self.attr_values[attr.get_name()] = new_value + self.push_change_event(attr.get_name(), new_value) + + +# -------------------------------------------------------------------- +def assert_enum(initial_value, readout_value): + if type(readout_value) in [list, tuple]: + for _initial_value, _readout_value in zip(initial_value, readout_value): + assert_enum(_initial_value, _readout_value) + else: + assert initial_value == readout_value + +# -------------------------------------------------------------------- +# fixtures to run Echo device +# -------------------------------------------------------------------- +@pytest.fixture(scope="session") +def echo_device(): + with MultiDeviceTestContext([{"class": EchoDevice, "devices": [{"name": "test/device/1"}]}], process=True) as context: + yield context.get_device_access("test/device/1") + + +# -------------------------------------------------------------------- +@pytest.fixture(autouse=True) +def reset_tango_asyncio(): + set_global_executor(None) + + +# -------------------------------------------------------------------- +# helpers to run tests +# -------------------------------------------------------------------- +def get_test_descriptor(python_type: Type[T], value: T) -> dict: + if python_type in [bool, int]: + return dict(dtype="integer", shape=[]) + if python_type in [float]: + return dict(dtype="number", shape=[]) + if python_type in [str]: + return dict(dtype="string", shape=[]) + if issubclass(python_type, (Enum, DevState)): + return dict(dtype="string", shape=[], choices=get_enum_labels(value.__class__)) + + return dict(dtype="array", shape=list(np.array(value).shape)) + + +# -------------------------------------------------------------------- +async def make_backend(typ: Optional[Type], pv: str, connect=True) -> SignalBackend: + backend = TangoTransport(typ, pv, pv) + if connect: + await asyncio.wait_for(backend.connect(), 10) + return backend + + +# -------------------------------------------------------------------- +def prepare_device(echo_device: str, pv: str, put_value: T) -> None: + setattr(DeviceProxy(echo_device), pv, put_value) + + +# -------------------------------------------------------------------- +class MonitorQueue: + def __init__(self, backend: SignalBackend): + self.backend = backend + self.subscription = backend.set_callback(self.add_reading_value) + self.updates: asyncio.Queue[Tuple[Reading, Any]] = asyncio.Queue() + + def add_reading_value(self, reading: Reading, value): + self.updates.put_nowait((reading, value)) + + async def assert_updates(self, expected_value): + expected_reading = { + "timestamp": pytest.approx(time.time(), rel=0.1), + "alarm_severity": 0, + } + update_reading, update_value = await self.updates.get() + get_reading = await self.backend.get_reading() + assert_close(update_value, expected_value) + assert_close(await self.backend.get_value(), expected_value) + + update_reading = dict(update_reading) + update_value = update_reading.pop("value") + + get_reading = dict(get_reading) + get_value = get_reading.pop("value") + + assert update_reading == expected_reading == get_reading + assert_close(update_value, expected_value) + assert_close(get_value, expected_value) + + def close(self): + self.backend.set_callback(None) + + +# -------------------------------------------------------------------- +async def assert_monitor_then_put( + echo_device: str, + pv: str, + initial_value: T, + put_value: T, + descriptor: dict, + datatype: Optional[Type[T]] = None, +): + prepare_device(echo_device, pv, initial_value) + source = echo_device + '/' + pv + backend = await make_backend(datatype, source) + # Make a monitor queue that will monitor for updates + q = MonitorQueue(backend) + try: + assert dict(source=source, **descriptor) == await backend.get_descriptor() + # Check initial value + await q.assert_updates(initial_value) + # Put to new value and check that + await backend.put(put_value) + await q.assert_updates(put_value) + finally: + q.close() + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize("pv, tango_type, d_format, py_type, initial_value, put_value", TYPES_TO_TEST) +async def test_backend_get_put_monitor(echo_device: str, + pv: str, + tango_type: str, + d_format: AttrDataFormat, + py_type: Type[T], + initial_value: T, + put_value: T): + # With the given datatype, check we have the correct initial value and putting + # works + descriptor = get_test_descriptor(py_type, initial_value) + await assert_monitor_then_put(echo_device, pv, initial_value, put_value, descriptor, py_type) + # # With datatype guessed from CA/PVA, check we can set it back to the initial value + await assert_monitor_then_put(echo_device, pv, initial_value, put_value, descriptor, datatype=None) From 957defe71bcd19219da150606768763faf2125a3 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 26 Feb 2024 09:25:59 +0100 Subject: [PATCH 002/141] rebasing --- .../tango/_backend/_tango_transport.py | 154 +++++++++---- tests/tango/test_signals.py | 216 ++++++++++-------- 2 files changed, 230 insertions(+), 140 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 66478bc82d..3493344a57 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -1,4 +1,6 @@ import asyncio +import time + import numpy as np from asyncio import CancelledError @@ -6,9 +8,9 @@ from enum import Enum from typing import Any, Dict, List, Optional, Sequence, Type, Union -from tango import AttributeConfig, AttrDataFormat, CmdArgType, EventType, GreenMode, DevState +from tango import AttributeInfoEx, AttrDataFormat, CmdArgType, EventType, GreenMode, DevState, CommandInfo from tango.asyncio import DeviceProxy -from tango.utils import is_int, is_float, is_bool, is_str, is_binary +from tango.utils import is_int, is_float, is_bool, is_str, is_binary, is_array from bluesky.protocols import Descriptor, Dtype, Reading @@ -24,25 +26,26 @@ # -------------------------------------------------------------------- -def get_pyton_type(tango_type): - if is_int(tango_type): - return int, "integer" - if is_float(tango_type): - return float, "number" - if is_bool(tango_type): - return bool, "integer" - if is_str(tango_type): - return str, "string" - if is_binary(tango_type): - return list[str], "string" +def get_pyton_type(tango_type) -> tuple[bool, T, str]: + array = is_array(tango_type) + if is_int(tango_type, True): + return array, int, "integer" + if is_float(tango_type, True): + return array, float, "number" + if is_bool(tango_type, True): + return array, bool, "integer" + if is_str(tango_type, True): + return array, str, "string" + if is_binary(tango_type, True): + return array, list[str], "string" if tango_type == CmdArgType.DevEnum: - return Enum, "string" + return array, Enum, "string" if tango_type == CmdArgType.DevState: - return CmdArgType.DevState, "string" + return array, CmdArgType.DevState, "string" if tango_type == CmdArgType.DevUChar: - return int, "integer" + return array, int, "integer" if tango_type == CmdArgType.DevVoid: - return None, "string" + return array, None, "string" raise TypeError("Unknown TangoType") @@ -67,7 +70,7 @@ async def put(self, value: Optional[T], wait: bool=True, timeout: Optional[float # -------------------------------------------------------------------- @abstractmethod - async def get_config(self) -> AttributeConfig: + async def get_config(self) -> Union[AttributeInfoEx, CommandInfo]: """Get value from PV""" # -------------------------------------------------------------------- @@ -106,16 +109,16 @@ async def get(self) -> T: async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None) -> None: if wait: if timeout: - id = self._proxy.write_attribute_asynch(self._name, value, green_mode=GreenMode.Synchronous) + rid = self._proxy.write_attribute_asynch(self._name, value, green_mode=GreenMode.Synchronous) await asyncio.sleep(timeout) - self._proxy.write_attribute_reply(id, green_mode=GreenMode.Synchronous) + self._proxy.write_attribute_reply(rid, green_mode=GreenMode.Synchronous) else: self._proxy.write_attribute(self._name, value, green_mode=GreenMode.Synchronous) else: - await self._proxy.write_attribute(self._name, value, wait=wait, timeout=timeout) + await self._proxy.write_attribute(self._name, value) # -------------------------------------------------------------------- - async def get_config(self) -> AttributeConfig: + async def get_config(self) -> AttributeInfoEx: return await self._proxy.get_attribute_config(self._name) # -------------------------------------------------------------------- @@ -155,11 +158,36 @@ def _event_processor(self, event): # -------------------------------------------------------------------- -async def get_tango_pv(full_trl: str, device_proxy: Optional[DeviceProxy]) -> TangoProxy: - device_trl, pv_name = full_trl.rsplit('/', 1) - device_proxy = device_proxy or await DeviceProxy(device_trl) - if pv_name in device_proxy.get_attribute_list(): - return AttributeProxy(device_proxy, pv_name) +class CommandProxy(TangoProxy): + + support_events = False + _last_reading = dict(value=None, timestamp=0, alarm_severity=0) + + # -------------------------------------------------------------------- + async def get(self) -> T: + return self._last_reading["value"] + + # -------------------------------------------------------------------- + async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None) -> None: + if wait: + if timeout: + rid = self._proxy.command_inout_asynch(self._name, value, green_mode=GreenMode.Synchronous) + await asyncio.sleep(timeout) + val = self._proxy.command_inout_reply(rid, green_mode=GreenMode.Synchronous) + else: + val = self._proxy.command_inout(self._name, value, green_mode=GreenMode.Synchronous) + else: + val = await self._proxy.command_inout(self._name, value) + + self._last_reading = dict(value=val, timestamp=time.time(), alarm_severity=0) + + # -------------------------------------------------------------------- + async def get_config(self) -> CommandInfo: + return await self._proxy.get_command_config(self._name) + + # -------------------------------------------------------------------- + async def get_reading(self) -> Reading: + return self._last_reading # -------------------------------------------------------------------- @@ -167,20 +195,41 @@ def get_dtype_extendet(datatype): # DevState tango type does not have numpy equivalents dtype = get_dtype(datatype) if dtype == np.object_: + print(f"{datatype.__args__[1].__args__[0]=}, {datatype.__args__[1].__args__[0]==Enum}") if datatype.__args__[1].__args__[0] == DevState: dtype = CmdArgType.DevState return dtype # -------------------------------------------------------------------- -def get_descriptor(datatype: Optional[Type], pv: str, attr_config: Dict[str, AttributeConfig]) -> dict: +def get_pv_descriptor(datatype: Optional[Type], pv: str, pvs_config: Dict[str, Union[AttributeInfoEx, CommandInfo]]) -> dict: + pvs_dtype = {} + for pv_name, config in pvs_config.items(): + if isinstance(config, AttributeInfoEx): + _, dtype, descr = get_pyton_type(config.data_type) + pvs_dtype[pv_name] = config.data_format, dtype, descr + elif isinstance(config, CommandInfo): + if config.in_type != CmdArgType.DevVoid and \ + config.out_type != CmdArgType.DevVoid and \ + config.in_type != config.out_type: + raise RuntimeError("Commands with different in and out dtypes are not supported") + array, dtype, descr = get_pyton_type(config.in_type if config.in_type != CmdArgType.DevVoid else config.out_type) + pvs_dtype[pv_name] = AttrDataFormat.SPECTRUM if array else AttrDataFormat.SCALAR, dtype, descr + else: + raise RuntimeError(f"Unknown config type: {type(config)}") + pv_format, pv_dtype, pv_dtype_desc = get_unique(pvs_dtype, "typeids") - pv_dtype, pv_dtype_desc = get_unique({k: get_pyton_type(v.data_type) for k, v in attr_config.items()}, "typeids") - attr_config = list(attr_config.values())[0] + # tango commands are limited in functionality: they do not have info about shape and Enum labels + pv_config = list(pvs_config.values())[0] + max_x = pv_config.max_dim_x if hasattr(pv_config, "max_dim_x") else np.Inf + max_y = pv_config.max_dim_x if hasattr(pv_config, "max_dim_y") else np.Inf + is_attr = hasattr(pv_config, "enum_labels") + pv_choices = list(pv_config.enum_labels) if is_attr else [] - if attr_config.data_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]: + if pv_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]: # This is an array if datatype: + print(f"{datatype=}") # Check we wanted an array of this type dtype = get_dtype_extendet(datatype) if not dtype: @@ -188,22 +237,20 @@ def get_descriptor(datatype: Optional[Type], pv: str, attr_config: Dict[str, Att if dtype != pv_dtype: raise TypeError(f"{pv} has type [{pv_dtype}] not [{dtype}]") - if attr_config.data_format == AttrDataFormat.SPECTRUM: - return dict(source=pv, dtype="array", shape=[attr_config.max_dim_x]) - else: - return dict(source=pv, dtype="array", shape=[attr_config.max_dim_y, attr_config.max_dim_x]) + if pv_format == AttrDataFormat.SPECTRUM: + return dict(source=pv, dtype="array", shape=[max_x]) + elif pv_format == AttrDataFormat.IMAGE: + return dict(source=pv, dtype="array", shape=[max_x, max_y]) else: if pv_dtype in (Enum, CmdArgType.DevState): - if pv_dtype == Enum: - pv_choices = list(attr_config.enum_labels) - else: + if pv_dtype == CmdArgType.DevState: pv_choices = list(DevState.names.keys()) if datatype: if not issubclass(datatype, (Enum, DevState)): raise TypeError(f"{pv} has type Enum not {datatype.__name__}") - if pv_dtype == Enum: + if pv_dtype == Enum and is_attr: choices = tuple(v.name for v in datatype) if set(choices) != set(pv_choices): raise TypeError(f"{pv} has choices {pv_choices} not {choices}") @@ -214,6 +261,20 @@ def get_descriptor(datatype: Optional[Type], pv: str, attr_config: Dict[str, Att return dict(source=pv, dtype=pv_dtype_desc, shape=[]) +# -------------------------------------------------------------------- +async def get_tango_pv(full_trl: str, device_proxy: Optional[DeviceProxy]) -> TangoProxy: + device_trl, pv_name = full_trl.rsplit('/', 1) + device_proxy = device_proxy or await DeviceProxy(device_trl) + if pv_name in device_proxy.get_attribute_list(): + return AttributeProxy(device_proxy, pv_name) + if pv_name in device_proxy.get_command_list(): + return CommandProxy(device_proxy, pv_name) + if pv_name in device_proxy.get_pipe_list(): + raise NotImplemented("Pipes are not supported") + + raise RuntimeError(f"{pv_name} cannot be found in {device_proxy.name()}") + + # -------------------------------------------------------------------- class TangoTransport(SignalBackend[T]): @@ -226,17 +287,15 @@ def __init__(self, self.read_pv = read_pv self.write_pv = write_pv self.proxies: Dict[str, TangoProxy] = {read_pv: device_proxy, write_pv: device_proxy} - self.initial_readings: Dict[str, Any] = {} self.pv_configs: Dict[str, AttributeConfig] = {} self.source = f"{self.read_pv}" self.descriptor: Descriptor = {} # type: ignore self.eid: Optional[int] = None # -------------------------------------------------------------------- - async def _connect_and_store_initial_value(self, pv): + async def _connect_and_store_config(self, pv): try: self.proxies[pv] = await get_tango_pv(pv, self.proxies[pv]) - self.initial_readings[pv] = await self.proxies[pv].get() self.pv_configs[pv] = await self.proxies[pv].get_config() except CancelledError: raise NotConnected(self.source) @@ -246,19 +305,16 @@ async def connect(self): if self.read_pv != self.write_pv: # Different, need to connect both await wait_for_connection( - read_pv=self._connect_and_store_initial_value(self.read_pv), - write_pv=self._connect_and_store_initial_value(self.write_pv), + read_pv=self._connect_and_store_config(self.read_pv), + write_pv=self._connect_and_store_config(self.write_pv), ) else: # The same, so only need to connect one - await self._connect_and_store_initial_value(self.read_pv) - self.descriptor = get_descriptor(self.datatype, self.read_pv, self.pv_configs) + await self._connect_and_store_config(self.read_pv) + self.descriptor = get_pv_descriptor(self.datatype, self.read_pv, self.pv_configs) # -------------------------------------------------------------------- async def put(self, write_value: Optional[T], wait=True, timeout=None): - if write_value is None: - write_value = self.initial_readings[self.write_pv]["value"] - await self.proxies[self.write_pv].put(write_value, wait, timeout) # -------------------------------------------------------------------- diff --git a/tests/tango/test_signals.py b/tests/tango/test_signals.py index 4ba9e7c239..6897e2b282 100644 --- a/tests/tango/test_signals.py +++ b/tests/tango/test_signals.py @@ -1,16 +1,18 @@ import pytest import asyncio import time +import textwrap import numpy as np import numpy.typing as npt +from random import choice from typing import Any, Optional, Tuple, Type from enum import Enum, IntEnum from tango import AttrWriteType, AttrDataFormat, DeviceProxy, DevState -from tango.server import Device, attribute +from tango.server import Device, attribute, command from tango.test_utils import assert_close from tango.test_context import MultiDeviceTestContext from tango.asyncio_executor import set_global_executor @@ -33,81 +35,45 @@ class TestEnum(IntEnum): B = 1 -def get_enum_labels(enum_cls): - if enum_cls == DevState: - return list(enum_cls.names.keys()) +BASE_TYPES_SET = ( + # type_name, tango_name, py_type, sample_values + ("boolean", 'DevBoolean', bool, (True, False)), + ("short", 'DevShort', int, (1, 2, 3, 4, 5)), + ("ushort", 'DevUShort', int, (1, 2, 3, 4, 5)), + ("long", 'DevLong', int, (1, 2, 3, 4, 5)), + ("ulong", 'DevULong', int, (1, 2, 3, 4, 5)), + ("long64", 'DevLong64', int, (1, 2, 3, 4, 5)), + ("char", 'DevUChar', int, (1, 2, 3, 4, 5)), + ("float", 'DevFloat', float, (1.1, 2.2, 3.3, 4.4, 5.5)), + ("double", 'DevDouble', float, (1.1, 2.2, 3.3, 4.4, 5.5)), + ("string", 'DevString', str, ('aaa', 'bbb', 'ccc')), + ("state", 'DevState', DevState, (DevState.ON, DevState.MOVING, DevState.ALARM)), + ("enum", 'DevEnum', TestEnum, (TestEnum.A, TestEnum.B)), + # ("encoded", 'DevEncoded', TestEnum, (TestEnum.A, TestEnum.B)), +) + +ATTRIBUTES_SET = [] +COMMANDS_SET = [] + +for type_name, tango_type_name, py_type, values in BASE_TYPES_SET: + ATTRIBUTES_SET.extend([ + (f"{type_name}_scalar_attr", tango_type_name, AttrDataFormat.SCALAR, py_type, choice(values), choice(values)), + (f"{type_name}_spectrum_attr", tango_type_name, AttrDataFormat.SPECTRUM, npt.NDArray[py_type], + [choice(values), choice(values)], [choice(values), choice(values)]), + (f"{type_name}_image_attr", tango_type_name, AttrDataFormat.IMAGE, npt.NDArray[py_type], + [[choice(values), choice(values)], [choice(values), choice(values)]], + [[choice(values), choice(values)], [choice(values), choice(values)]]) + ]) + + if tango_type_name == 'DevUChar': + continue else: - return [member.name for member in enum_cls] - - -TEST_ENUM_CLASS_LABELS = get_enum_labels(TestEnum) - - -TYPES_TO_TEST = ( - ("boolean_scalar", 'DevBoolean', AttrDataFormat.SCALAR, bool, True, False), - ("boolean_spectrum", 'DevBoolean', AttrDataFormat.SPECTRUM, npt.NDArray[np.bool_], [True, False], [False, True]), - ("boolean_image", 'DevBoolean', AttrDataFormat.IMAGE, npt.NDArray[np.bool_], [[True, False], [False, True]], - np.array([[False, True], [True, False]])), - - ("short_scalar", 'DevShort', AttrDataFormat.SCALAR, int, 1, 2), - ("short_spectrum", 'DevShort', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), - ("short_image", 'DevShort', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), - - ("long_scalar", 'DevLong', AttrDataFormat.SCALAR, int, 1, 2), - ("long_spectrum", 'DevLong', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), - ("long_image", 'DevLong', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), - - ("ushort_scalar", 'DevUShort', AttrDataFormat.SCALAR, int, 1, 2), - ("ushort_spectrum", 'DevUShort', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), - ("ushort_image", 'DevUShort', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), - - ("ulong_scalar", 'DevLong', AttrDataFormat.SCALAR, int, 1, 2), - ("ulong_spectrum", 'DevLong', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), - ("ulong_image", 'DevLong', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), - - ("ulong64_scalar", 'DevULong64', AttrDataFormat.SCALAR, int, 1, 2), - ("ulong64_spectrum", 'DevULong64', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), - ("ulong64_image", 'DevULong64', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), - - ("long64_scalar", 'DevLong64', AttrDataFormat.SCALAR, int, 1, 2), - ("long64_spectrum", 'DevLong64', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), - ("long64_image", 'DevLong64', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), - - ("char_scalar", 'DevUChar', AttrDataFormat.SCALAR, int, 1, 2), - ("char_spectrum", 'DevUChar', AttrDataFormat.SPECTRUM, npt.NDArray[np.int_], [1, 2], [3, 4]), - ("char_image", 'DevUChar', AttrDataFormat.IMAGE, npt.NDArray[np.int_], [[1, 2], [3, 4]], [[5, 6], [7, 8]]), - - ("float_scalar", 'DevFloat', AttrDataFormat.SCALAR, float, 1.1, 2.2), - ("float_spectrum", 'DevFloat', AttrDataFormat.SPECTRUM, npt.NDArray[np.float_], [1.1, 2.2], [3.3, 4.4]), - ("float_image", 'DevFloat', AttrDataFormat.IMAGE, npt.NDArray[np.float_], [[1.1, 2.2], [3.3, 4.4]], - [[5.5, 6.6], [7.7, 8.8]]), - - ("double_scalar", 'DevDouble', AttrDataFormat.SCALAR, float, 1.1, 2.2), - ("double_spectrum", 'DevDouble', AttrDataFormat.SPECTRUM, npt.NDArray[np.float_], [1.1, 2.2], [3.3, 4.4]), - ("double_image", 'DevDouble', AttrDataFormat.IMAGE, npt.NDArray[np.float_], [[1.1, 2.2], [3.3, 4.4]], - [[5.5, 6.6], [7.7, 8.8]]), - - ("string_scalar", 'DevString', AttrDataFormat.SCALAR, str, "aaa", "bbb"), - ("string_spectrum", 'DevString', AttrDataFormat.SPECTRUM, npt.NDArray[str], ["aaa", "bbb"], ["ccc", "ddd"]), - ("string_image", 'DevString', AttrDataFormat.IMAGE, npt.NDArray[str], [["aaa", "bbb"], ["ccc", "ddd"]], - [["eee", "fff"], ["ggg", "hhh"]]), - - ("state_scalar", 'DevState', AttrDataFormat.SCALAR, DevState, DevState.ON, DevState.OFF), - ("state_spectrum", 'DevState', AttrDataFormat.SPECTRUM, npt.NDArray[DevState], [DevState.ON, DevState.OFF], - [DevState.OFF, DevState.ON]), - ("state_image", 'DevState', AttrDataFormat.IMAGE, npt.NDArray[DevState], - [[DevState.ON, DevState.OFF], [DevState.OFF, DevState.ON]], - [[DevState.OFF, DevState.ON], [DevState.ON, DevState.OFF]]), - - ("enum_scalar", 'DevEnum', AttrDataFormat.SCALAR, TestEnum, TestEnum.A, TestEnum.B), - ("enum_spectrum", 'DevEnum', AttrDataFormat.SPECTRUM, npt.NDArray[TestEnum], [TestEnum.A, TestEnum.B], - [TestEnum.B, TestEnum.A]), - ("enum_image", 'DevEnum', AttrDataFormat.IMAGE, npt.NDArray[TestEnum], - [[TestEnum.A, TestEnum.B], [TestEnum.B, TestEnum.A]], - [[TestEnum.B, TestEnum.A], [TestEnum.A, TestEnum.B]]), - - # ('DevEncoded': tango._tango.CmdArgType.DevEncoded, - ) + COMMANDS_SET.append((f"{type_name}_scalar_cmd", tango_type_name, AttrDataFormat.SCALAR, py_type, choice(values), choice(values))) + if tango_type_name in ['DevState', 'DevEnum']: + continue + else: + COMMANDS_SET.append((f"{type_name}_spectrum_cmd", tango_type_name, AttrDataFormat.SPECTRUM, + npt.NDArray[py_type], [choice(values), choice(values)], [choice(values), choice(values)])) # -------------------------------------------------------------------- @@ -117,7 +83,7 @@ class EchoDevice(Device): attr_values = {} def initialize_dynamic_attributes(self): - for name, typ, form, _, _, _ in TYPES_TO_TEST: + for name, typ, form, _, _, _ in ATTRIBUTES_SET: attr = attribute( name=name, dtype=typ, @@ -127,11 +93,21 @@ def initialize_dynamic_attributes(self): fset=self.write, max_dim_x=2, max_dim_y=2, - enum_labels=TEST_ENUM_CLASS_LABELS + enum_labels=[member.name for member in TestEnum] ) self.add_attribute(attr) self.set_change_event(name, True, False) + for name, typ, form, _, _, _ in COMMANDS_SET: + cmd = command( + f=getattr(self, name), + dtype_in=typ, + dformat_in=form, + dtype_out=typ, + dformat_out=form + ) + self.add_command(cmd) + def read(self, attr): attr.set_value(self.attr_values[attr.get_name()]) @@ -140,6 +116,15 @@ def write(self, attr): self.attr_values[attr.get_name()] = new_value self.push_change_event(attr.get_name(), new_value) + echo_command_code = textwrap.dedent( + """\ + def echo_command(self, arg): + return arg + """ + ) + + for name, _, _, _, _, _ in COMMANDS_SET: + exec(echo_command_code.replace("echo_command", name)) # -------------------------------------------------------------------- def assert_enum(initial_value, readout_value): @@ -167,17 +152,19 @@ def reset_tango_asyncio(): # -------------------------------------------------------------------- # helpers to run tests # -------------------------------------------------------------------- -def get_test_descriptor(python_type: Type[T], value: T) -> dict: +def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: if python_type in [bool, int]: return dict(dtype="integer", shape=[]) if python_type in [float]: return dict(dtype="number", shape=[]) if python_type in [str]: return dict(dtype="string", shape=[]) - if issubclass(python_type, (Enum, DevState)): - return dict(dtype="string", shape=[], choices=get_enum_labels(value.__class__)) + if issubclass(python_type, DevState): + return dict(dtype="string", shape=[], choices=list(DevState.names.keys())) + if issubclass(python_type, Enum): + return dict(dtype="string", shape=[], choices=[] if is_cmd else [member.name for member in value.__class__]) - return dict(dtype="array", shape=list(np.array(value).shape)) + return dict(dtype="array", shape=[np.Inf] if is_cmd else list(np.array(value).shape)) # -------------------------------------------------------------------- @@ -254,17 +241,64 @@ async def assert_monitor_then_put( # -------------------------------------------------------------------- @pytest.mark.asyncio -@pytest.mark.parametrize("pv, tango_type, d_format, py_type, initial_value, put_value", TYPES_TO_TEST) -async def test_backend_get_put_monitor(echo_device: str, - pv: str, - tango_type: str, - d_format: AttrDataFormat, - py_type: Type[T], - initial_value: T, - put_value: T): +@pytest.mark.parametrize("pv, tango_type, d_format, py_type, initial_value, put_value", ATTRIBUTES_SET, + ids=[x[0] for x in ATTRIBUTES_SET]) +async def test_backend_get_put_monitor_attr(echo_device: str, + pv: str, + tango_type: str, + d_format: AttrDataFormat, + py_type: Type[T], + initial_value: T, + put_value: T): # With the given datatype, check we have the correct initial value and putting # works - descriptor = get_test_descriptor(py_type, initial_value) + descriptor = get_test_descriptor(py_type, initial_value, False) await assert_monitor_then_put(echo_device, pv, initial_value, put_value, descriptor, py_type) # # With datatype guessed from CA/PVA, check we can set it back to the initial value - await assert_monitor_then_put(echo_device, pv, initial_value, put_value, descriptor, datatype=None) + await assert_monitor_then_put(echo_device, pv, initial_value, put_value, descriptor) + + +# -------------------------------------------------------------------- +async def assert_put_read( + echo_device: str, + pv: str, + put_value: T, + descriptor: dict, + datatype: Optional[Type[T]] = None, +): + source = echo_device + '/' + pv + backend = await make_backend(datatype, source) + # Make a monitor queue that will monitor for updates + assert dict(source=source, **descriptor) == await backend.get_descriptor() + # Put to new value and check that + await backend.put(put_value) + + expected_reading = { + "timestamp": pytest.approx(time.time(), rel=0.1), + "alarm_severity": 0, + } + + assert_close(await backend.get_value(), put_value) + + get_reading = dict(await backend.get_reading()) + get_reading.pop('value') + assert expected_reading == get_reading + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize("pv, tango_type, d_format, py_type, initial_value, put_value", COMMANDS_SET, + ids=[x[0] for x in COMMANDS_SET]) +async def test_backend_get_put_monitor_cmd(echo_device: str, + pv: str, + tango_type: str, + d_format: AttrDataFormat, + py_type: Type[T], + initial_value: T, + put_value: T): + # With the given datatype, check we have the correct initial value and putting + # works + descriptor = get_test_descriptor(py_type, initial_value, True) + await assert_put_read(echo_device, pv, put_value, descriptor, py_type) + # # With datatype guessed from CA/PVA, check we can set it back to the initial value + await assert_put_read(echo_device, pv, put_value, descriptor) \ No newline at end of file From e0e3b3c830be29b23613022e34b0d09b4d631fd3 Mon Sep 17 00:00:00 2001 From: matveyev Date: Thu, 19 Oct 2023 17:28:36 +0200 Subject: [PATCH 003/141] signals for attributes and commands and signal_test made generic TangoDevice and test for it --- src/ophyd_async/tango/__init__.py | 1 + .../tango/_backend/_tango_transport.py | 147 +++++++++--------- src/ophyd_async/tango/device/__init__.py | 3 + src/ophyd_async/tango/device/device.py | 123 +++++++++++++++ src/ophyd_async/tango/signal/__init__.py | 6 +- src/ophyd_async/tango/signal/signal.py | 66 ++++---- tests/tango/test_signals.py | 19 ++- 7 files changed, 256 insertions(+), 109 deletions(-) create mode 100644 src/ophyd_async/tango/device/__init__.py create mode 100644 src/ophyd_async/tango/device/device.py diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py index 412c0d9291..8f5116f664 100644 --- a/src/ophyd_async/tango/__init__.py +++ b/src/ophyd_async/tango/__init__.py @@ -1,4 +1,5 @@ from ophyd_async.tango.signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x +from ophyd_async.tango.device import TangoDevice __all__ = [ "tango_signal_r", diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 3493344a57..c014e2c60f 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -6,9 +6,13 @@ from abc import abstractmethod from enum import Enum -from typing import Any, Dict, List, Optional, Sequence, Type, Union +from typing import Dict, Optional, Type, Union + +from tango import (AttributeInfoEx, AttrDataFormat, + CmdArgType, EventType, + GreenMode, DevState, + CommandInfo, AttrWriteType) -from tango import AttributeInfoEx, AttrDataFormat, CmdArgType, EventType, GreenMode, DevState, CommandInfo from tango.asyncio import DeviceProxy from tango.utils import is_int, is_float, is_bool, is_str, is_binary, is_array @@ -24,6 +28,8 @@ wait_for_connection, ) +__all__ = ("TangoTransport",) + # -------------------------------------------------------------------- def get_pyton_type(tango_type) -> tuple[bool, T, str]: @@ -61,26 +67,26 @@ def __init__(self, device_proxy: DeviceProxy, name: str): # -------------------------------------------------------------------- @abstractmethod async def get(self) -> T: - """Get value from PV""" + """Get value from TRL""" # -------------------------------------------------------------------- @abstractmethod async def put(self, value: Optional[T], wait: bool=True, timeout: Optional[float]=None) -> None: - """Get value from PV""" + """Put value to TRL""" # -------------------------------------------------------------------- @abstractmethod async def get_config(self) -> Union[AttributeInfoEx, CommandInfo]: - """Get value from PV""" + """Get TRL config async""" # -------------------------------------------------------------------- @abstractmethod async def get_reading(self) -> Reading: - """Get value from PV""" + """Get reading from TRL""" # -------------------------------------------------------------------- def has_subscription(self) -> bool: - """indicates, that this pv already subscribed""" + """indicates, that this trl already subscribed""" # -------------------------------------------------------------------- @abstractmethod @@ -136,7 +142,7 @@ def has_subscription(self) -> bool: # -------------------------------------------------------------------- def subscribe_callback(self, callback: Optional[ReadingValueCallback]): - """add user callack to delete CHANGE event subscription""" + """add user callback to CHANGE event subscription""" self._event_callback = callback self._eid = self._proxy.subscribe_event(self._name, EventType.CHANGE_EVENT, self._event_processor) @@ -191,7 +197,7 @@ async def get_reading(self) -> Reading: # -------------------------------------------------------------------- -def get_dtype_extendet(datatype): +def get_dtype_extended(datatype): # DevState tango type does not have numpy equivalents dtype = get_dtype(datatype) if dtype == np.object_: @@ -202,77 +208,76 @@ def get_dtype_extendet(datatype): # -------------------------------------------------------------------- -def get_pv_descriptor(datatype: Optional[Type], pv: str, pvs_config: Dict[str, Union[AttributeInfoEx, CommandInfo]]) -> dict: - pvs_dtype = {} - for pv_name, config in pvs_config.items(): +def get_trl_descriptor(datatype: Optional[Type], trl: str, trls_config: Dict[str, Union[AttributeInfoEx, CommandInfo]]) -> dict: + trls_dtype = {} + for trl_name, config in trls_config.items(): if isinstance(config, AttributeInfoEx): _, dtype, descr = get_pyton_type(config.data_type) - pvs_dtype[pv_name] = config.data_format, dtype, descr + trls_dtype[trl_name] = config.data_format, dtype, descr elif isinstance(config, CommandInfo): if config.in_type != CmdArgType.DevVoid and \ config.out_type != CmdArgType.DevVoid and \ config.in_type != config.out_type: raise RuntimeError("Commands with different in and out dtypes are not supported") array, dtype, descr = get_pyton_type(config.in_type if config.in_type != CmdArgType.DevVoid else config.out_type) - pvs_dtype[pv_name] = AttrDataFormat.SPECTRUM if array else AttrDataFormat.SCALAR, dtype, descr + trls_dtype[trl_name] = AttrDataFormat.SPECTRUM if array else AttrDataFormat.SCALAR, dtype, descr else: raise RuntimeError(f"Unknown config type: {type(config)}") - pv_format, pv_dtype, pv_dtype_desc = get_unique(pvs_dtype, "typeids") + trl_format, trl_dtype, trl_dtype_desc = get_unique(trls_dtype, "typeids") # tango commands are limited in functionality: they do not have info about shape and Enum labels - pv_config = list(pvs_config.values())[0] - max_x = pv_config.max_dim_x if hasattr(pv_config, "max_dim_x") else np.Inf - max_y = pv_config.max_dim_x if hasattr(pv_config, "max_dim_y") else np.Inf - is_attr = hasattr(pv_config, "enum_labels") - pv_choices = list(pv_config.enum_labels) if is_attr else [] + trl_config = list(trls_config.values())[0] + max_x = trl_config.max_dim_x if hasattr(trl_config, "max_dim_x") else np.Inf + max_y = trl_config.max_dim_y if hasattr(trl_config, "max_dim_y") else np.Inf + is_attr = hasattr(trl_config, "enum_labels") + trl_choices = list(trl_config.enum_labels) if is_attr else [] - if pv_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]: + if trl_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]: # This is an array if datatype: - print(f"{datatype=}") # Check we wanted an array of this type - dtype = get_dtype_extendet(datatype) + dtype = get_dtype_extended(datatype) if not dtype: - raise TypeError(f"{pv} has type [{pv_dtype}] not {datatype.__name__}") - if dtype != pv_dtype: - raise TypeError(f"{pv} has type [{pv_dtype}] not [{dtype}]") + raise TypeError(f"{trl} has type [{trl_dtype}] not {datatype.__name__}") + if dtype != trl_dtype: + raise TypeError(f"{trl} has type [{trl_dtype}] not [{dtype}]") - if pv_format == AttrDataFormat.SPECTRUM: - return dict(source=pv, dtype="array", shape=[max_x]) - elif pv_format == AttrDataFormat.IMAGE: - return dict(source=pv, dtype="array", shape=[max_x, max_y]) + if trl_format == AttrDataFormat.SPECTRUM: + return dict(source=trl, dtype="array", shape=[max_x]) + elif trl_format == AttrDataFormat.IMAGE: + return dict(source=trl, dtype="array", shape=[max_y, max_x]) else: - if pv_dtype in (Enum, CmdArgType.DevState): - if pv_dtype == CmdArgType.DevState: - pv_choices = list(DevState.names.keys()) + if trl_dtype in (Enum, CmdArgType.DevState): + if trl_dtype == CmdArgType.DevState: + trl_choices = list(DevState.names.keys()) if datatype: if not issubclass(datatype, (Enum, DevState)): - raise TypeError(f"{pv} has type Enum not {datatype.__name__}") - if pv_dtype == Enum and is_attr: + raise TypeError(f"{trl} has type Enum not {datatype.__name__}") + if trl_dtype == Enum and is_attr: choices = tuple(v.name for v in datatype) - if set(choices) != set(pv_choices): - raise TypeError(f"{pv} has choices {pv_choices} not {choices}") - return dict(source=pv, dtype="string", shape=[], choices=pv_choices) + if set(choices) != set(trl_choices): + raise TypeError(f"{trl} has choices {trl_choices} not {choices}") + return dict(source=trl, dtype="string", shape=[], choices=trl_choices) else: - if datatype and not issubclass(pv_dtype, datatype): - raise TypeError(f"{pv} has type {pv_dtype.__name__} not {datatype.__name__}") - return dict(source=pv, dtype=pv_dtype_desc, shape=[]) + if datatype and not issubclass(trl_dtype, datatype): + raise TypeError(f"{trl} has type {trl_dtype.__name__} not {datatype.__name__}") + return dict(source=trl, dtype=trl_dtype_desc, shape=[]) # -------------------------------------------------------------------- -async def get_tango_pv(full_trl: str, device_proxy: Optional[DeviceProxy]) -> TangoProxy: - device_trl, pv_name = full_trl.rsplit('/', 1) +async def get_tango_trl(full_trl: str, device_proxy: Optional[DeviceProxy]) -> TangoProxy: + device_trl, trl_name = full_trl.rsplit('/', 1) device_proxy = device_proxy or await DeviceProxy(device_trl) - if pv_name in device_proxy.get_attribute_list(): - return AttributeProxy(device_proxy, pv_name) - if pv_name in device_proxy.get_command_list(): - return CommandProxy(device_proxy, pv_name) - if pv_name in device_proxy.get_pipe_list(): + if trl_name in device_proxy.get_attribute_list(): + return AttributeProxy(device_proxy, trl_name) + if trl_name in device_proxy.get_command_list(): + return CommandProxy(device_proxy, trl_name) + if trl_name in device_proxy.get_pipe_list(): raise NotImplemented("Pipes are not supported") - raise RuntimeError(f"{pv_name} cannot be found in {device_proxy.name()}") + raise RuntimeError(f"{trl_name} cannot be found in {device_proxy.name()}") # -------------------------------------------------------------------- @@ -280,42 +285,42 @@ class TangoTransport(SignalBackend[T]): def __init__(self, datatype: Optional[Type[T]], - read_pv: str, - write_pv: str, + read_trl: str, + write_trl: str, device_proxy: Optional[DeviceProxy] = None): self.datatype = datatype - self.read_pv = read_pv - self.write_pv = write_pv - self.proxies: Dict[str, TangoProxy] = {read_pv: device_proxy, write_pv: device_proxy} - self.pv_configs: Dict[str, AttributeConfig] = {} - self.source = f"{self.read_pv}" + self.read_trl = read_trl + self.write_trl = write_trl + self.proxies: Dict[str, TangoProxy] = {read_trl: device_proxy, write_trl: device_proxy} + self.trl_configs: Dict[str, AttributeInfoEx] = {} + self.source = f"{self.read_trl}" self.descriptor: Descriptor = {} # type: ignore self.eid: Optional[int] = None # -------------------------------------------------------------------- - async def _connect_and_store_config(self, pv): + async def _connect_and_store_config(self, trl): try: - self.proxies[pv] = await get_tango_pv(pv, self.proxies[pv]) - self.pv_configs[pv] = await self.proxies[pv].get_config() + self.proxies[trl] = await get_tango_trl(trl, self.proxies[trl]) + self.trl_configs[trl] = await self.proxies[trl].get_config() except CancelledError: raise NotConnected(self.source) # -------------------------------------------------------------------- async def connect(self): - if self.read_pv != self.write_pv: + if self.read_trl != self.write_trl: # Different, need to connect both await wait_for_connection( - read_pv=self._connect_and_store_config(self.read_pv), - write_pv=self._connect_and_store_config(self.write_pv), + read_trl=self._connect_and_store_config(self.read_trl), + write_trl=self._connect_and_store_config(self.write_trl), ) else: # The same, so only need to connect one - await self._connect_and_store_config(self.read_pv) - self.descriptor = get_pv_descriptor(self.datatype, self.read_pv, self.pv_configs) + await self._connect_and_store_config(self.read_trl) + self.descriptor = get_trl_descriptor(self.datatype, self.read_trl, self.trl_configs) # -------------------------------------------------------------------- async def put(self, write_value: Optional[T], wait=True, timeout=None): - await self.proxies[self.write_pv].put(write_value, wait, timeout) + await self.proxies[self.write_trl].put(write_value, wait, timeout) # -------------------------------------------------------------------- async def get_descriptor(self) -> Descriptor: @@ -323,21 +328,21 @@ async def get_descriptor(self) -> Descriptor: # -------------------------------------------------------------------- async def get_reading(self) -> Reading: - return await self.proxies[self.read_pv].get_reading() + return await self.proxies[self.read_trl].get_reading() # -------------------------------------------------------------------- async def get_value(self) -> T: - return await self.proxies[self.write_pv].get() + return await self.proxies[self.write_trl].get() # -------------------------------------------------------------------- def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: - assert self.proxies[self.read_pv].support_events, f"{self.source} does not support events" + assert self.proxies[self.read_trl].support_events, f"{self.source} does not support events" if callback: - assert (not self.proxies[self.read_pv].has_subscription()), "Cannot set a callback when one is already set" - self.eid = self.proxies[self.read_pv].subscribe_callback(callback) + assert (not self.proxies[self.read_trl].has_subscription()), "Cannot set a callback when one is already set" + self.eid = self.proxies[self.read_trl].subscribe_callback(callback) else: if self.eid: - self.proxies[self.read_pv].unsubscribe_callback(self.eid) + self.proxies[self.read_trl].unsubscribe_callback(self.eid) self.eid = None \ No newline at end of file diff --git a/src/ophyd_async/tango/device/__init__.py b/src/ophyd_async/tango/device/__init__.py new file mode 100644 index 0000000000..e7dbaabe68 --- /dev/null +++ b/src/ophyd_async/tango/device/__init__.py @@ -0,0 +1,3 @@ +from .device import TangoDevice, ReadableSignal, ReadableUncachedSignal, ConfigurableSignal + +__all__ = ("TangoDevice",) diff --git a/src/ophyd_async/tango/device/device.py b/src/ophyd_async/tango/device/device.py new file mode 100644 index 0000000000..3afbfeb291 --- /dev/null +++ b/src/ophyd_async/tango/device/device.py @@ -0,0 +1,123 @@ +"""Default Tango Devices""" + +from __future__ import annotations + +from inspect import isclass +from typing import get_type_hints, Union, Generic, get_origin, get_args + +from tango import AttrWriteType, CmdArgType +from tango.asyncio import DeviceProxy + +from ophyd_async.core import StandardReadable, SignalW, SignalX, SignalR, SignalRW, T +from ophyd_async.tango.signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x + +__all__ = ("TangoDevice", + "ReadableSignal", + "ReadableUncachedSignal", + "ConfigurableSignal") + + +# -------------------------------------------------------------------- + +class _AutoSignal: + """ + To mark attribute/command in devices type hits as the one, which has to be read every step + """ + + +# -------------------------------------------------------------------- +class ReadableSignal(Generic[T], _AutoSignal): + """ + To mark attribute/command in devices type hits as the one, which has to be read every step + """ + + _add_me_to = "_read_signals" + + +# -------------------------------------------------------------------- +class ReadableUncachedSignal(Generic[T], _AutoSignal): + """ + To mark attribute/command in devices type hits as the one, which has to be read every step + """ + + _add_me_to = "_read_signals" + + +# -------------------------------------------------------------------- +class ConfigurableSignal(Generic[T], _AutoSignal): + """ + To mark attribute/command in devices type hits as the one, which has to be read only as startup + """ + + +# -------------------------------------------------------------------- +def get_signal(dtype: T, name: str, trl: str, device_proxy: DeviceProxy) -> Union[SignalW, SignalX, SignalR, SignalRW]: + ftrl = trl + '/' + name + if name in device_proxy.get_attribute_list(): + conf = device_proxy.get_attribute_config(name, green_mode=False) + + if conf.writable in [AttrWriteType.READ_WRITE, AttrWriteType.READ_WITH_WRITE]: + return tango_signal_rw(dtype, ftrl, ftrl, device_proxy) + elif conf.writable == AttrWriteType.READ: + return tango_signal_r(dtype, ftrl, device_proxy) + else: + return tango_signal_w(dtype, ftrl, device_proxy) + if name in device_proxy.get_command_list(): + # TODO: check logic + conf = device_proxy.get_command_config(name, green_mode=False) + if conf.in_type == CmdArgType.DevVoid: + return tango_signal_x(ftrl, device_proxy) + elif conf.out_type != CmdArgType.DevVoid: + return tango_signal_rw(dtype, ftrl, ftrl, device_proxy) + else: + return tango_signal_r(dtype, ftrl, device_proxy) + + if name in device_proxy.get_pipe_list(): + raise NotImplemented("Pipes are not supported") + + raise RuntimeError(f"{name} cannot be found in {device_proxy.name()}") + + +# -------------------------------------------------------------------- +class TangoDevice(StandardReadable): + """ + General class for TangoDevices + + Usage: to proper signals mount should be awaited: + + new_device = await TangoDevice() + """ + + # -------------------------------------------------------------------- + def __init__(self, trl: str, name="") -> None: + self.trl = trl + self.proxy: DeviceProxy = None + + super().__init__(name=name) + + # -------------------------------------------------------------------- + def __await__(self): + async def closure(): + self.proxy = await DeviceProxy(self.trl) + hints = get_type_hints(self) + signals = {} + for name, dtype in hints.items(): + add_to = None + origin = get_origin(dtype) + if isclass(origin) and issubclass(origin, _AutoSignal): + add_to = origin._add_me_to + dtype = get_args(dtype)[0] + signal = get_signal(dtype, name, self.trl, self.proxy) # type: ignore + setattr(self, name, signal) + if add_to: + if add_to not in signals: + signals[add_to] = [getattr(self, name)] + else: + signals[add_to].append(getattr(self, name)) + for name, signals_set in signals.items(): + setattr(self, name, tuple(signals_set)) + return self + + return closure().__await__() + + diff --git a/src/ophyd_async/tango/signal/__init__.py b/src/ophyd_async/tango/signal/__init__.py index 6ca2a387a1..df44991efa 100644 --- a/src/ophyd_async/tango/signal/__init__.py +++ b/src/ophyd_async/tango/signal/__init__.py @@ -1,8 +1,8 @@ from .signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x -__all__ = [ +__all__ = ( "tango_signal_r", "tango_signal_rw", "tango_signal_w", - "tango_signal_x", -] + "tango_signal_x" +) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 2c417453db..10d2a68eed 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Optional, Tuple, Type +from typing import Optional, Type from tango import DeviceProxy from ophyd_async.core import ( @@ -14,80 +14,90 @@ T, ) -from .._backend import TangoTransport +__all__ = ("tango_signal_rw", + "tango_signal_r", + "tango_signal_w", + "tango_signal_x") +from ophyd_async.tango._backend import TangoTransport + + +# -------------------------------------------------------------------- def tango_signal_rw(datatype: Type[T], - read_pv: str, - write_pv: Optional[str] = None, + read_trl: str, + write_trl: Optional[str] = None, device_proxy: Optional[DeviceProxy] = None ) -> SignalRW[T]: - """Create a `SignalRW` backed by 1 or 2 EPICS PVs + """Create a `SignalRW` backed by 1 or 2 Tango Attribute/Command Parameters ---------- datatype: - Check that the PV is of this type - read_pv: - The PV to read and monitor - write_pv: - If given, use this PV to write to, otherwise use read_pv + Check that the Attribute/Command is of this type + read_trl: + The Attribute/Command to read and monitor + write_trl: + If given, use this Attribute/Command to write to, otherwise use read_trl device_proxy: If given, this DeviceProxy will be used """ - backend = TangoTransport(datatype, read_pv, write_pv or read_pv, device_proxy) + backend = TangoTransport(datatype, read_trl, write_trl or read_trl, device_proxy) return SignalRW(backend) +# -------------------------------------------------------------------- def tango_signal_r(datatype: Type[T], - read_pv: str, + read_trl: str, device_proxy: Optional[DeviceProxy] = None ) -> SignalR[T]: - """Create a `SignalR` backed by 1 EPICS PV + """Create a `SignalR` backed by 1 Tango Attribute/Command Parameters ---------- datatype: - Check that the PV is of this type - read_pv: - The PV to read and monitor + Check that the Attribute/Command is of this type + read_trl: + The Attribute/Command to read and monitor device_proxy: If given, this DeviceProxy will be used """ - backend = TangoTransport(datatype, read_pv, read_pv, device_proxy) + backend = TangoTransport(datatype, read_trl, read_trl, device_proxy) return SignalR(backend) +# -------------------------------------------------------------------- def tango_signal_w(datatype: Type[T], - write_pv: str, + write_trl: str, device_proxy: Optional[DeviceProxy] = None ) -> SignalW[T]: - """Create a `SignalW` backed by 1 EPICS PVs + """Create a `SignalW` backed by 1 Tango Attribute/Command Parameters ---------- datatype: - Check that the PV is of this type - write_pv: - The PV to write to + Check that the Attribute/Command is of this type + write_trl: + The Attribute/Command to write to device_proxy: If given, this DeviceProxy will be used """ - backend = TangoTransport(datatype, write_pv, write_pv, device_proxy) + backend = TangoTransport(datatype, write_trl, write_trl, device_proxy) return SignalW(backend) -def tango_signal_x(write_pv: str, +# -------------------------------------------------------------------- +def tango_signal_x(write_trl: str, device_proxy: Optional[DeviceProxy] = None ) -> SignalX: - """Create a `SignalX` backed by 1 EPICS PVs + """Create a `SignalX` backed by 1 Tango Attribute/Command Parameters ---------- - write_pv: - The PV to write its initial value to on execute + write_trl: + The Attribute/Command to write its initial value to on execute device_proxy: If given, this DeviceProxy will be used """ - backend: SignalBackend = TangoTransport(None, write_pv, write_pv, device_proxy) + backend: SignalBackend = TangoTransport(None, write_trl, write_trl, device_proxy) return SignalX(backend) diff --git a/tests/tango/test_signals.py b/tests/tango/test_signals.py index 6897e2b282..8d2778c313 100644 --- a/tests/tango/test_signals.py +++ b/tests/tango/test_signals.py @@ -59,21 +59,24 @@ class TestEnum(IntEnum): ATTRIBUTES_SET.extend([ (f"{type_name}_scalar_attr", tango_type_name, AttrDataFormat.SCALAR, py_type, choice(values), choice(values)), (f"{type_name}_spectrum_attr", tango_type_name, AttrDataFormat.SPECTRUM, npt.NDArray[py_type], - [choice(values), choice(values)], [choice(values), choice(values)]), + [choice(values), choice(values), choice(values)], [choice(values), choice(values), choice(values)]), (f"{type_name}_image_attr", tango_type_name, AttrDataFormat.IMAGE, npt.NDArray[py_type], - [[choice(values), choice(values)], [choice(values), choice(values)]], - [[choice(values), choice(values)], [choice(values), choice(values)]]) + [[choice(values), choice(values), choice(values)], [choice(values), choice(values), choice(values)]], + [[choice(values), choice(values), choice(values)], [choice(values), choice(values), choice(values)]]) ]) if tango_type_name == 'DevUChar': continue else: - COMMANDS_SET.append((f"{type_name}_scalar_cmd", tango_type_name, AttrDataFormat.SCALAR, py_type, choice(values), choice(values))) + COMMANDS_SET.append((f"{type_name}_scalar_cmd", tango_type_name, AttrDataFormat.SCALAR, + py_type, choice(values), choice(values))) if tango_type_name in ['DevState', 'DevEnum']: continue else: COMMANDS_SET.append((f"{type_name}_spectrum_cmd", tango_type_name, AttrDataFormat.SPECTRUM, - npt.NDArray[py_type], [choice(values), choice(values)], [choice(values), choice(values)])) + npt.NDArray[py_type], + [choice(values), choice(values), choice(values)], + [choice(values), choice(values), choice(values)])) # -------------------------------------------------------------------- @@ -91,7 +94,7 @@ def initialize_dynamic_attributes(self): access=AttrWriteType.READ_WRITE, fget=self.read, fset=self.write, - max_dim_x=2, + max_dim_x=3, max_dim_y=2, enum_labels=[member.name for member in TestEnum] ) @@ -126,6 +129,7 @@ def echo_command(self, arg): for name, _, _, _, _, _ in COMMANDS_SET: exec(echo_command_code.replace("echo_command", name)) + # -------------------------------------------------------------------- def assert_enum(initial_value, readout_value): if type(readout_value) in [list, tuple]: @@ -134,12 +138,13 @@ def assert_enum(initial_value, readout_value): else: assert initial_value == readout_value + # -------------------------------------------------------------------- # fixtures to run Echo device # -------------------------------------------------------------------- @pytest.fixture(scope="session") def echo_device(): - with MultiDeviceTestContext([{"class": EchoDevice, "devices": [{"name": "test/device/1"}]}], process=True) as context: + with MultiDeviceTestContext([{"class": EchoDevice, "devices": [{"name": "test/device/1"}]}]) as context: yield context.get_device_access("test/device/1") From 94b5c376189569ac73dc143be7f3204d2b864003 Mon Sep 17 00:00:00 2001 From: matveyev Date: Thu, 19 Oct 2023 17:29:06 +0200 Subject: [PATCH 004/141] test for generic TangoDevice --- tests/tango/test_device.py | 156 +++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 tests/tango/test_device.py diff --git a/tests/tango/test_device.py b/tests/tango/test_device.py new file mode 100644 index 0000000000..2dffaaf36f --- /dev/null +++ b/tests/tango/test_device.py @@ -0,0 +1,156 @@ +import time + +import pytest + +from typing import get_type_hints, Any, Optional, Tuple, Type +from enum import Enum, IntEnum + +import numpy as np +import numpy.typing as npt + +from tango import AttrQuality, AttrDataFormat, AttrWriteType, DeviceProxy, DevState, CmdArgType +from tango.test_utils import assert_close +from tango.server import Device, attribute, command +from tango.asyncio_executor import set_global_executor +from tango.test_context import MultiDeviceTestContext + +from ophyd_async.tango._backend._tango_transport import get_pyton_type +from ophyd_async.tango.device import TangoDevice, ReadableSignal +from ophyd_async.core import DeviceCollector, T + + +class TestEnum(IntEnum): + A = 0 + B = 1 + +# -------------------------------------------------------------------- +# fixtures to run Echo device +# -------------------------------------------------------------------- +class TestDevice(Device): + + _array = [[1, 2, 3], + [4, 5, 6]] + + _limitedvalue = 3 + + @attribute(dtype=int) + def justvalue(self): + return 5 + + @attribute(dtype=float, access=AttrWriteType.READ_WRITE, + dformat=AttrDataFormat.IMAGE, max_dim_x=3, max_dim_y=2) + def array(self) -> list[list[float]]: + return self._array + + def write_array(self, array: list[list[float]]): + self._array = array + + @attribute(dtype=float, access=AttrWriteType.READ_WRITE, + min_value=0, min_alarm=1, min_warning=2, + max_warning=4, max_alarm=5, max_value=6) + def limitedvalue(self) -> float: + return self._limitedvalue + + def write_limitedvalue(self, value: float): + self._limitedvalue = value + + +# -------------------------------------------------------------------- +def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: + if python_type in [bool, int]: + return dict(dtype="integer", shape=[]) + if python_type in [float]: + return dict(dtype="number", shape=[]) + if python_type in [str]: + return dict(dtype="string", shape=[]) + if issubclass(python_type, DevState): + return dict(dtype="string", shape=[], choices=list(DevState.names.keys())) + if issubclass(python_type, Enum): + return dict(dtype="string", shape=[], choices=[] if is_cmd else [member.name for member in value.__class__]) + + return dict(dtype="array", shape=[np.Inf] if is_cmd else list(np.array(value).shape)) + + +# -------------------------------------------------------------------- +@pytest.fixture(scope="session") +def tango_test_device(): + with MultiDeviceTestContext([{"class": TestDevice, "devices": [{"name": "test/device/1"}]}]) as context: + yield context.get_device_access("test/device/1") + + +# -------------------------------------------------------------------- +@pytest.fixture(autouse=True) +def reset_tango_asyncio(): + set_global_executor(None) + + +# -------------------------------------------------------------------- +class TestReadableDevice(TangoDevice): + justvalue: ReadableSignal[int] + array: ReadableSignal[npt.NDArray[float]] + limitedvalue: ReadableSignal[float] + + +# -------------------------------------------------------------------- +def describe_class(cls, fqtrl): + description = {} + values = {} + dev = DeviceProxy(fqtrl) + hints = get_type_hints(cls) + for name, dtype in hints.items(): + if name in dev.get_attribute_list(): + attr_conf = dev.get_attribute_config(name) + value = dev.read_attribute(name).value + _, _, descr = get_pyton_type(attr_conf.data_type) + max_x = attr_conf.max_dim_x + max_y = attr_conf.max_dim_y + if attr_conf.data_format == AttrDataFormat.SCALAR: + is_array = False + shape = [] + elif attr_conf.data_format == AttrDataFormat.SPECTRUM: + is_array = True + shape = [max_x] + else: + is_array = True + shape = [max_y, max_x] + + elif name in dev.get_command_list(): + cmd_conf = dev.get_command_config(name) + _, _, descr = get_pyton_type(cmd_conf.in_type if cmd_conf.in_type != CmdArgType.DevVoid else cmd_conf.out_type) + shape = [] + is_array = False + value = getattr(dev, name)() + + else: + raise RuntimeError(f"{name} cannot be found in {cls} attributes/commands") + + description[f"test_device-{name}"] = {'source': f'{fqtrl}/{name}', # type: ignore + 'dtype': 'array' if is_array else descr, + 'shape': shape} + + values[f"test_device-{name}"] = {'value': value, + 'timestamp': pytest.approx(time.time()), + 'alarm_severity': AttrQuality.ATTR_VALID} + + return values, description + + +# -------------------------------------------------------------------- +def compare_values(expected, received): + assert set(list(expected.keys())) == set(list(received.keys())) + for k, v in expected.items(): + for _k, _v in v.items(): + assert_close(_v, received[k][_k]) + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_connect(tango_test_device): + values, description = describe_class(TestReadableDevice(""), tango_test_device) + + async with DeviceCollector(): + test_device = await TestReadableDevice(tango_test_device) + + assert test_device.name == "test_device" + assert description == await test_device.describe() + compare_values(values, await test_device.read()) From 3b6547e9355ae049c27a1aca42f775bc7471c189 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 26 Feb 2024 09:26:39 +0100 Subject: [PATCH 005/141] rebasing --- src/ophyd_async/tango/device/__init__.py | 6 ++- src/ophyd_async/tango/device/device.py | 67 +++++++++++++++++++----- tests/tango/test_device.py | 13 +++-- tests/tango/test_signals.py | 6 ++- 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/src/ophyd_async/tango/device/__init__.py b/src/ophyd_async/tango/device/__init__.py index e7dbaabe68..9873cbc16c 100644 --- a/src/ophyd_async/tango/device/__init__.py +++ b/src/ophyd_async/tango/device/__init__.py @@ -1,3 +1,5 @@ -from .device import TangoDevice, ReadableSignal, ReadableUncachedSignal, ConfigurableSignal +from .device import (TangoDevice, TangoStandardReadableDevice, + ReadableSignal, ReadableUncachedSignal, ConfigurableSignal) -__all__ = ("TangoDevice",) +__all__ = ("TangoDevice", "TangoStandardReadableDevice", + "ReadableSignal", "ReadableUncachedSignal", "ConfigurableSignal") diff --git a/src/ophyd_async/tango/device/device.py b/src/ophyd_async/tango/device/device.py index 3afbfeb291..7432a501dd 100644 --- a/src/ophyd_async/tango/device/device.py +++ b/src/ophyd_async/tango/device/device.py @@ -3,15 +3,16 @@ from __future__ import annotations from inspect import isclass -from typing import get_type_hints, Union, Generic, get_origin, get_args +from typing import get_type_hints, Union, Generic, Tuple, Sequence, Type, get_origin, get_args from tango import AttrWriteType, CmdArgType from tango.asyncio import DeviceProxy -from ophyd_async.core import StandardReadable, SignalW, SignalX, SignalR, SignalRW, T +from ophyd_async.core import StandardReadable, SignalW, SignalX, SignalR, SignalRW, Signal, T from ophyd_async.tango.signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x __all__ = ("TangoDevice", + "TangoStandardReadableDevice", "ReadableSignal", "ReadableUncachedSignal", "ConfigurableSignal") @@ -19,14 +20,14 @@ # -------------------------------------------------------------------- -class _AutoSignal: +class _AutoSignal(Signal): """ To mark attribute/command in devices type hits as the one, which has to be read every step """ # -------------------------------------------------------------------- -class ReadableSignal(Generic[T], _AutoSignal): +class ReadableSignal(_AutoSignal, Generic[T]): """ To mark attribute/command in devices type hits as the one, which has to be read every step """ @@ -35,23 +36,25 @@ class ReadableSignal(Generic[T], _AutoSignal): # -------------------------------------------------------------------- -class ReadableUncachedSignal(Generic[T], _AutoSignal): +class ReadableUncachedSignal(_AutoSignal, Generic[T]): """ To mark attribute/command in devices type hits as the one, which has to be read every step """ - _add_me_to = "_read_signals" + _add_me_to = "_read_uncached_signals" # -------------------------------------------------------------------- -class ConfigurableSignal(Generic[T], _AutoSignal): +class ConfigurableSignal(_AutoSignal, Generic[T]): """ To mark attribute/command in devices type hits as the one, which has to be read only as startup """ + _add_me_to = "_configuration_signals" + # -------------------------------------------------------------------- -def get_signal(dtype: T, name: str, trl: str, device_proxy: DeviceProxy) -> Union[SignalW, SignalX, SignalR, SignalRW]: +def get_signal(dtype: Type[T], name: str, trl: str, device_proxy: DeviceProxy) -> Union[SignalW, SignalX, SignalR, SignalRW]: ftrl = trl + '/' + name if name in device_proxy.get_attribute_list(): conf = device_proxy.get_attribute_config(name, green_mode=False) @@ -79,7 +82,7 @@ def get_signal(dtype: T, name: str, trl: str, device_proxy: DeviceProxy) -> Unio # -------------------------------------------------------------------- -class TangoDevice(StandardReadable): +class TangoDevice: """ General class for TangoDevices @@ -89,12 +92,10 @@ class TangoDevice(StandardReadable): """ # -------------------------------------------------------------------- - def __init__(self, trl: str, name="") -> None: + def __init__(self, trl: str) -> None: self.trl = trl self.proxy: DeviceProxy = None - super().__init__(name=name) - # -------------------------------------------------------------------- def __await__(self): async def closure(): @@ -116,8 +117,50 @@ async def closure(): signals[add_to].append(getattr(self, name)) for name, signals_set in signals.items(): setattr(self, name, tuple(signals_set)) + + self.register_signals() + return self return closure().__await__() + # -------------------------------------------------------------------- + def register_signals(self): + """ + This method can be used to manually register signals + """ + + +# -------------------------------------------------------------------- +class TangoStandardReadableDevice(TangoDevice, StandardReadable): + + _read_signals: Tuple[Union[SignalR, SignalRW], ...] = () + _configuration_signals: Tuple[Union[SignalR, SignalRW], ...] = () + _read_uncached_signals: Tuple[Union[SignalR, SignalRW], ...] = () + + # -------------------------------------------------------------------- + def __init__(self, trl: str, name="") -> None: + TangoDevice.__init__(self, trl) + StandardReadable.__init__(self, name=name) + + # -------------------------------------------------------------------- + def set_readable_signals( + self, + read: Sequence[Union[SignalR, SignalRW]] = (), + config: Sequence[Union[SignalR, SignalRW]] = (), + read_uncached: Sequence[Union[SignalR, SignalRW]] = (), + ): + """ + Parameters + ---------- + read: + Signals to make up :meth:`~StandardReadable.read` + config: + Signals to make up :meth:`~StandardReadable.read_configuration` + read_uncached: + Signals to make up :meth:`~StandardReadable.read` that won't be cached + """ + self._read_signals = tuple(read) + self._configuration_signals = tuple(config) + self._read_uncached_signals = tuple(read_uncached) diff --git a/tests/tango/test_device.py b/tests/tango/test_device.py index 2dffaaf36f..ff6045f578 100644 --- a/tests/tango/test_device.py +++ b/tests/tango/test_device.py @@ -15,18 +15,21 @@ from tango.test_context import MultiDeviceTestContext from ophyd_async.tango._backend._tango_transport import get_pyton_type -from ophyd_async.tango.device import TangoDevice, ReadableSignal +from ophyd_async.tango.device import TangoStandardReadableDevice, ReadableSignal from ophyd_async.core import DeviceCollector, T class TestEnum(IntEnum): + __test__ = False A = 0 B = 1 + # -------------------------------------------------------------------- # fixtures to run Echo device # -------------------------------------------------------------------- class TestDevice(Device): + __test__ = False _array = [[1, 2, 3], [4, 5, 6]] @@ -72,9 +75,10 @@ def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: # -------------------------------------------------------------------- -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def tango_test_device(): - with MultiDeviceTestContext([{"class": TestDevice, "devices": [{"name": "test/device/1"}]}]) as context: + with MultiDeviceTestContext( + [{"class": TestDevice, "devices": [{"name": "test/device/1"}]}], process=True) as context: yield context.get_device_access("test/device/1") @@ -85,7 +89,8 @@ def reset_tango_asyncio(): # -------------------------------------------------------------------- -class TestReadableDevice(TangoDevice): +class TestReadableDevice(TangoStandardReadableDevice): + __test__ = False justvalue: ReadableSignal[int] array: ReadableSignal[npt.NDArray[float]] limitedvalue: ReadableSignal[float] diff --git a/tests/tango/test_signals.py b/tests/tango/test_signals.py index 8d2778c313..57a299bbb5 100644 --- a/tests/tango/test_signals.py +++ b/tests/tango/test_signals.py @@ -31,6 +31,7 @@ class TestEnum(IntEnum): + __test__ = False A = 0 B = 1 @@ -142,9 +143,10 @@ def assert_enum(initial_value, readout_value): # -------------------------------------------------------------------- # fixtures to run Echo device # -------------------------------------------------------------------- -@pytest.fixture(scope="session") +@pytest.fixture(scope="module") def echo_device(): - with MultiDeviceTestContext([{"class": EchoDevice, "devices": [{"name": "test/device/1"}]}]) as context: + with MultiDeviceTestContext( + [{"class": EchoDevice, "devices": [{"name": "test/device/1"}]}], process=True) as context: yield context.get_device_access("test/device/1") From 4874c440ac5331ae208fd4c266a6175c4362ea9e Mon Sep 17 00:00:00 2001 From: matveyev Date: Fri, 20 Oct 2023 15:05:06 +0200 Subject: [PATCH 006/141] added "TangoSignalRW", "TangoSignalW", "TangoSignalBackend" - they have additional methods to get last written value --- src/ophyd_async/tango/_backend/__init__.py | 4 +- .../tango/_backend/_tango_transport.py | 50 +++++++++++++++++-- src/ophyd_async/tango/signal/signal.py | 24 +++------ tests/tango/test_signals.py | 5 +- 4 files changed, 61 insertions(+), 22 deletions(-) diff --git a/src/ophyd_async/tango/_backend/__init__.py b/src/ophyd_async/tango/_backend/__init__.py index 2c3b266408..f33e54220c 100644 --- a/src/ophyd_async/tango/_backend/__init__.py +++ b/src/ophyd_async/tango/_backend/__init__.py @@ -1 +1,3 @@ -from ._tango_transport import TangoTransport \ No newline at end of file +from ._tango_transport import TangoTransport, TangoSignalRW, TangoSignalW, TangoSignalBackend + +__all__ = ("TangoTransport", "TangoSignalRW", "TangoSignalW", "TangoSignalBackend") \ No newline at end of file diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index c014e2c60f..af62092f39 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -16,19 +16,49 @@ from tango.asyncio import DeviceProxy from tango.utils import is_int, is_float, is_bool, is_str, is_binary, is_array -from bluesky.protocols import Descriptor, Dtype, Reading +from bluesky.protocols import Descriptor, Reading from ophyd_async.core import ( NotConnected, ReadingValueCallback, SignalBackend, + SignalRW, + SignalW, T, get_dtype, get_unique, wait_for_connection, ) -__all__ = ("TangoTransport",) +from ophyd_async.core._device._signal.signal import _add_timeout + +__all__ = ("TangoTransport", "TangoSignalRW", "TangoSignalW", "TangoSignalBackend") + + +# -------------------------------------------------------------------- +# from tango attributes one can get setvalue, so we extend SignalRW and SignalW to add it +class SignalWithSetpoit: + @_add_timeout + async def get_setpoint(self, cached: Optional[bool] = None) -> T: + """The last written value to TRL""" + return await self._backend_or_cache(cached).get_w_value() + + +# -------------------------------------------------------------------- +class TangoSignalW(SignalW[T], SignalWithSetpoit): + ... + + +# -------------------------------------------------------------------- +class TangoSignalRW(SignalRW[T], SignalWithSetpoit): + ... + + +# -------------------------------------------------------------------- +class TangoSignalBackend(SignalBackend[T]): + @abstractmethod + async def get_w_value(self) -> T: + """The last written value""" # -------------------------------------------------------------------- @@ -69,6 +99,11 @@ def __init__(self, device_proxy: DeviceProxy, name: str): async def get(self) -> T: """Get value from TRL""" + # -------------------------------------------------------------------- + @abstractmethod + async def get_w_value(self) -> T: + """Get last written value from TRL""" + # -------------------------------------------------------------------- @abstractmethod async def put(self, value: Optional[T], wait: bool=True, timeout: Optional[float]=None) -> None: @@ -111,6 +146,11 @@ async def get(self) -> T: attr = await self._proxy.read_attribute(self._name) return attr.value + # -------------------------------------------------------------------- + async def get_w_value(self) -> T: + attr = await self._proxy.read_attribute(self._name) + return attr.w_value + # -------------------------------------------------------------------- async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None) -> None: if wait: @@ -281,7 +321,7 @@ async def get_tango_trl(full_trl: str, device_proxy: Optional[DeviceProxy]) -> T # -------------------------------------------------------------------- -class TangoTransport(SignalBackend[T]): +class TangoTransport(TangoSignalBackend[T]): def __init__(self, datatype: Optional[Type[T]], @@ -334,6 +374,10 @@ async def get_reading(self) -> Reading: async def get_value(self) -> T: return await self.proxies[self.write_trl].get() + # -------------------------------------------------------------------- + async def get_w_value(self) -> T: + return await self.proxies[self.write_trl].get_w_value() + # -------------------------------------------------------------------- def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: assert self.proxies[self.read_trl].support_events, f"{self.source} does not support events" diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 10d2a68eed..179a7b1daa 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -5,14 +5,9 @@ from typing import Optional, Type from tango import DeviceProxy -from ophyd_async.core import ( - SignalBackend, - SignalR, - SignalRW, - SignalW, - SignalX, - T, -) +from ophyd_async.core import SignalR, SignalX, T + +from ophyd_async.tango._backend import TangoTransport, TangoSignalRW, TangoSignalW, TangoSignalBackend __all__ = ("tango_signal_rw", "tango_signal_r", @@ -20,15 +15,12 @@ "tango_signal_x") -from ophyd_async.tango._backend import TangoTransport - - # -------------------------------------------------------------------- def tango_signal_rw(datatype: Type[T], read_trl: str, write_trl: Optional[str] = None, device_proxy: Optional[DeviceProxy] = None - ) -> SignalRW[T]: + ) -> TangoSignalRW[T]: """Create a `SignalRW` backed by 1 or 2 Tango Attribute/Command Parameters @@ -43,7 +35,7 @@ def tango_signal_rw(datatype: Type[T], If given, this DeviceProxy will be used """ backend = TangoTransport(datatype, read_trl, write_trl or read_trl, device_proxy) - return SignalRW(backend) + return TangoSignalRW(backend) # -------------------------------------------------------------------- @@ -70,7 +62,7 @@ def tango_signal_r(datatype: Type[T], def tango_signal_w(datatype: Type[T], write_trl: str, device_proxy: Optional[DeviceProxy] = None - ) -> SignalW[T]: + ) -> TangoSignalW[T]: """Create a `SignalW` backed by 1 Tango Attribute/Command Parameters @@ -83,7 +75,7 @@ def tango_signal_w(datatype: Type[T], If given, this DeviceProxy will be used """ backend = TangoTransport(datatype, write_trl, write_trl, device_proxy) - return SignalW(backend) + return TangoSignalW(backend) # -------------------------------------------------------------------- @@ -99,5 +91,5 @@ def tango_signal_x(write_trl: str, device_proxy: If given, this DeviceProxy will be used """ - backend: SignalBackend = TangoTransport(None, write_trl, write_trl, device_proxy) + backend: TangoSignalBackend = TangoTransport(None, write_trl, write_trl, device_proxy) return SignalX(backend) diff --git a/tests/tango/test_signals.py b/tests/tango/test_signals.py index 57a299bbb5..f37a1a621f 100644 --- a/tests/tango/test_signals.py +++ b/tests/tango/test_signals.py @@ -20,7 +20,7 @@ from bluesky.protocols import Reading from ophyd_async.core import SignalBackend, T -from ophyd_async.tango._backend import TangoTransport +from ophyd_async.tango._backend import TangoTransport, TangoSignalBackend # -------------------------------------------------------------------- @@ -175,7 +175,7 @@ def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: # -------------------------------------------------------------------- -async def make_backend(typ: Optional[Type], pv: str, connect=True) -> SignalBackend: +async def make_backend(typ: Optional[Type], pv: str, connect=True) -> TangoSignalBackend: backend = TangoTransport(typ, pv, pv) if connect: await asyncio.wait_for(backend.connect(), 10) @@ -241,6 +241,7 @@ async def assert_monitor_then_put( await q.assert_updates(initial_value) # Put to new value and check that await backend.put(put_value) + assert_close(put_value, await backend.get_w_value()) await q.assert_updates(put_value) finally: q.close() From 969ff1f73989533e5b06c6fe3c7982e05c06d57b Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 26 Feb 2024 09:27:11 +0100 Subject: [PATCH 007/141] rebasing --- src/ophyd_async/core/flyer.py | 4 +- src/ophyd_async/core/signal.py | 8 +- src/ophyd_async/tango/__init__.py | 6 +- src/ophyd_async/tango/_backend/__init__.py | 5 +- .../tango/_backend/_signal_backend.py | 25 ++++ .../tango/_backend/_tango_transport.py | 101 +++++++------ src/ophyd_async/tango/device/__init__.py | 6 +- src/ophyd_async/tango/device/device.py | 136 ++---------------- src/ophyd_async/tango/sardana/__init__.py | 3 + src/ophyd_async/tango/sardana/devices.py | 87 +++++++++++ src/ophyd_async/tango/signal/__init__.py | 5 +- src/ophyd_async/tango/signal/signal.py | 43 +++++- tests/tango/test_device.py | 105 ++++++++------ tests/tango/test_sardana.py | 22 +++ tests/tango/test_signals.py | 18 +-- 15 files changed, 330 insertions(+), 244 deletions(-) create mode 100644 src/ophyd_async/tango/_backend/_signal_backend.py create mode 100644 src/ophyd_async/tango/sardana/__init__.py create mode 100644 src/ophyd_async/tango/sardana/devices.py create mode 100644 tests/tango/test_sardana.py diff --git a/src/ophyd_async/core/flyer.py b/src/ophyd_async/core/flyer.py index b6c83df4f6..86d368568b 100644 --- a/src/ophyd_async/core/flyer.py +++ b/src/ophyd_async/core/flyer.py @@ -15,7 +15,7 @@ from bluesky.protocols import ( Asset, - Collectable, + # Collectable, Descriptor, Flyable, HasHints, @@ -177,7 +177,7 @@ class HardwareTriggeredFlyable( Preparable, Stageable, Flyable, - Collectable, + # Collectable, WritesExternalAssets, HasHints, Generic[T], diff --git a/src/ophyd_async/core/signal.py b/src/ophyd_async/core/signal.py index d922cc0d9e..56d1da1a75 100644 --- a/src/ophyd_async/core/signal.py +++ b/src/ophyd_async/core/signal.py @@ -24,7 +24,7 @@ _sim_backends: Dict[Signal, SimSignalBackend] = {} -def _add_timeout(func): +def add_timeout(func): @functools.wraps(func) async def wrapper(self: Signal, *args, **kwargs): return await asyncio.wait_for(func(self, *args, **kwargs), self._timeout) @@ -160,17 +160,17 @@ def _del_cache(self, needed: bool): self._cache.close() self._cache = None - @_add_timeout + @add_timeout async def read(self, cached: Optional[bool] = None) -> Dict[str, Reading]: """Return a single item dict with the reading in it""" return {self.name: await self._backend_or_cache(cached).get_reading()} - @_add_timeout + @add_timeout async def describe(self) -> Dict[str, Descriptor]: """Return a single item dict with the descriptor in it""" return {self.name: await self._backend.get_descriptor()} - @_add_timeout + @add_timeout async def get_value(self, cached: Optional[bool] = None) -> T: """The current value""" return await self._backend_or_cache(cached).get_value() diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py index 8f5116f664..338ad1a455 100644 --- a/src/ophyd_async/tango/__init__.py +++ b/src/ophyd_async/tango/__init__.py @@ -1,9 +1,11 @@ -from ophyd_async.tango.signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x -from ophyd_async.tango.device import TangoDevice +from ophyd_async.tango.signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x, tango_signal_auto +from ophyd_async.tango.device import TangoReadableDevice __all__ = [ "tango_signal_r", "tango_signal_rw", "tango_signal_w", "tango_signal_x", + "tango_signal_auto", + "TangoReadableDevice" ] diff --git a/src/ophyd_async/tango/_backend/__init__.py b/src/ophyd_async/tango/_backend/__init__.py index f33e54220c..553e38db6c 100644 --- a/src/ophyd_async/tango/_backend/__init__.py +++ b/src/ophyd_async/tango/_backend/__init__.py @@ -1,3 +1,4 @@ -from ._tango_transport import TangoTransport, TangoSignalRW, TangoSignalW, TangoSignalBackend +from ophyd_async.tango._backend._tango_transport import TangoTransport, TangoSignalBackend +from ophyd_async.tango._backend._signal_backend import TangoSignalW, TangoSignalRW -__all__ = ("TangoTransport", "TangoSignalRW", "TangoSignalW", "TangoSignalBackend") \ No newline at end of file +__all__ = ("TangoTransport", "TangoSignalBackend", "TangoSignalW", "TangoSignalRW") \ No newline at end of file diff --git a/src/ophyd_async/tango/_backend/_signal_backend.py b/src/ophyd_async/tango/_backend/_signal_backend.py new file mode 100644 index 0000000000..e5a0357cd5 --- /dev/null +++ b/src/ophyd_async/tango/_backend/_signal_backend.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from typing import Optional + +from ophyd_async.core import T, SignalW, SignalRW +from ophyd_async.core.signal import add_timeout + + +# -------------------------------------------------------------------- +# from tango attributes one can get setvalue, so we extend SignalRW and SignalW to add it +class SignalWithSetpoit: + @add_timeout + async def get_setpoint(self, cached: Optional[bool] = None) -> T: + """The last written value to TRL""" + return await self._backend_or_cache(cached).get_w_value() + + +# -------------------------------------------------------------------- +class TangoSignalW(SignalW[T], SignalWithSetpoit): + ... + + +# -------------------------------------------------------------------- +class TangoSignalRW(SignalRW[T], SignalWithSetpoit): + ... diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index af62092f39..0eb6bdf845 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -1,5 +1,7 @@ import asyncio import time +import functools +import logging import numpy as np from asyncio import CancelledError @@ -14,6 +16,7 @@ CommandInfo, AttrWriteType) from tango.asyncio import DeviceProxy +from tango.asyncio_executor import get_global_executor, set_global_executor, AsyncioExecutor from tango.utils import is_int, is_float, is_bool, is_str, is_binary, is_array from bluesky.protocols import Descriptor, Reading @@ -22,44 +25,40 @@ NotConnected, ReadingValueCallback, SignalBackend, - SignalRW, - SignalW, T, get_dtype, get_unique, wait_for_connection, ) -from ophyd_async.core._device._signal.signal import _add_timeout - -__all__ = ("TangoTransport", "TangoSignalRW", "TangoSignalW", "TangoSignalBackend") +__all__ = ("TangoTransport", "TangoSignalBackend") # -------------------------------------------------------------------- -# from tango attributes one can get setvalue, so we extend SignalRW and SignalW to add it -class SignalWithSetpoit: - @_add_timeout - async def get_setpoint(self, cached: Optional[bool] = None) -> T: - """The last written value to TRL""" - return await self._backend_or_cache(cached).get_w_value() - -# -------------------------------------------------------------------- -class TangoSignalW(SignalW[T], SignalWithSetpoit): - ... +def ensure_proper_executor(func): + @functools.wraps(func) + async def wrapper(self, *args, **kwargs): + current_executor = get_global_executor() + if not current_executor.in_executor_context(): + set_global_executor(AsyncioExecutor()) + return await func(self, *args, **kwargs) - -# -------------------------------------------------------------------- -class TangoSignalRW(SignalRW[T], SignalWithSetpoit): - ... + return wrapper # -------------------------------------------------------------------- class TangoSignalBackend(SignalBackend[T]): + # -------------------------------------------------------------------- @abstractmethod async def get_w_value(self) -> T: """The last written value""" + # -------------------------------------------------------------------- + @abstractmethod + def get_signal_auto(self) -> str: + """return signal type, passing to tango attribute/command""" + # -------------------------------------------------------------------- def get_pyton_type(tango_type) -> tuple[bool, T, str]: @@ -142,16 +141,19 @@ class AttributeProxy(TangoProxy): _eid = None # -------------------------------------------------------------------- + @ensure_proper_executor async def get(self) -> T: attr = await self._proxy.read_attribute(self._name) return attr.value # -------------------------------------------------------------------- + @ensure_proper_executor async def get_w_value(self) -> T: attr = await self._proxy.read_attribute(self._name) return attr.w_value # -------------------------------------------------------------------- + @ensure_proper_executor async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None) -> None: if wait: if timeout: @@ -164,10 +166,12 @@ async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[flo await self._proxy.write_attribute(self._name, value) # -------------------------------------------------------------------- + @ensure_proper_executor async def get_config(self) -> AttributeInfoEx: return await self._proxy.get_attribute_config(self._name) # -------------------------------------------------------------------- + @ensure_proper_executor async def get_reading(self) -> Reading: attr = await self._proxy.read_attribute(self._name) return dict( @@ -184,11 +188,12 @@ def has_subscription(self) -> bool: def subscribe_callback(self, callback: Optional[ReadingValueCallback]): """add user callback to CHANGE event subscription""" self._event_callback = callback - self._eid = self._proxy.subscribe_event(self._name, EventType.CHANGE_EVENT, self._event_processor) + self._eid = self._proxy.subscribe_event(self._name, EventType.CHANGE_EVENT, self._event_processor, + green_mode=False) # -------------------------------------------------------------------- def unsubscribe_callback(self, eid: int): - self._proxy.unsubscribe_event(self._eid) + self._proxy.unsubscribe_event(self._eid, green_mode=False) self._eid = None self._event_callback = None @@ -214,6 +219,7 @@ async def get(self) -> T: return self._last_reading["value"] # -------------------------------------------------------------------- + # @ensure_proper_executor async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None) -> None: if wait: if timeout: @@ -228,6 +234,7 @@ async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[flo self._last_reading = dict(value=val, timestamp=time.time(), alarm_severity=0) # -------------------------------------------------------------------- + # @ensure_proper_executor async def get_config(self) -> CommandInfo: return await self._proxy.get_command_config(self._name) @@ -248,62 +255,64 @@ def get_dtype_extended(datatype): # -------------------------------------------------------------------- -def get_trl_descriptor(datatype: Optional[Type], trl: str, trls_config: Dict[str, Union[AttributeInfoEx, CommandInfo]]) -> dict: - trls_dtype = {} - for trl_name, config in trls_config.items(): +def get_trl_descriptor(datatype: Optional[Type], tango_resource: str, + tr_configs: Dict[str, Union[AttributeInfoEx, CommandInfo]]) -> dict: + tr_dtype = {} + for tr_name, config in tr_configs.items(): if isinstance(config, AttributeInfoEx): _, dtype, descr = get_pyton_type(config.data_type) - trls_dtype[trl_name] = config.data_format, dtype, descr + tr_dtype[tr_name] = config.data_format, dtype, descr elif isinstance(config, CommandInfo): if config.in_type != CmdArgType.DevVoid and \ config.out_type != CmdArgType.DevVoid and \ config.in_type != config.out_type: raise RuntimeError("Commands with different in and out dtypes are not supported") array, dtype, descr = get_pyton_type(config.in_type if config.in_type != CmdArgType.DevVoid else config.out_type) - trls_dtype[trl_name] = AttrDataFormat.SPECTRUM if array else AttrDataFormat.SCALAR, dtype, descr + tr_dtype[tr_name] = AttrDataFormat.SPECTRUM \ + if array else AttrDataFormat.SCALAR, dtype, descr else: raise RuntimeError(f"Unknown config type: {type(config)}") - trl_format, trl_dtype, trl_dtype_desc = get_unique(trls_dtype, "typeids") + tr_format, tr_dtype, tr_dtype_desc = get_unique(tr_dtype, "typeids") # tango commands are limited in functionality: they do not have info about shape and Enum labels - trl_config = list(trls_config.values())[0] + trl_config = list(tr_configs.values())[0] max_x = trl_config.max_dim_x if hasattr(trl_config, "max_dim_x") else np.Inf max_y = trl_config.max_dim_y if hasattr(trl_config, "max_dim_y") else np.Inf is_attr = hasattr(trl_config, "enum_labels") trl_choices = list(trl_config.enum_labels) if is_attr else [] - if trl_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]: + if tr_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]: # This is an array if datatype: # Check we wanted an array of this type dtype = get_dtype_extended(datatype) if not dtype: - raise TypeError(f"{trl} has type [{trl_dtype}] not {datatype.__name__}") - if dtype != trl_dtype: - raise TypeError(f"{trl} has type [{trl_dtype}] not [{dtype}]") + raise TypeError(f"{tango_resource} has type [{tr_dtype}] not {datatype.__name__}") + if dtype != tr_dtype: + raise TypeError(f"{tango_resource} has type [{tr_dtype}] not [{dtype}]") - if trl_format == AttrDataFormat.SPECTRUM: - return dict(source=trl, dtype="array", shape=[max_x]) - elif trl_format == AttrDataFormat.IMAGE: - return dict(source=trl, dtype="array", shape=[max_y, max_x]) + if tr_format == AttrDataFormat.SPECTRUM: + return dict(source=tango_resource, dtype="array", shape=[max_x]) + elif tr_format == AttrDataFormat.IMAGE: + return dict(source=tango_resource, dtype="array", shape=[max_y, max_x]) else: - if trl_dtype in (Enum, CmdArgType.DevState): - if trl_dtype == CmdArgType.DevState: + if tr_dtype in (Enum, CmdArgType.DevState): + if tr_dtype == CmdArgType.DevState: trl_choices = list(DevState.names.keys()) if datatype: if not issubclass(datatype, (Enum, DevState)): - raise TypeError(f"{trl} has type Enum not {datatype.__name__}") - if trl_dtype == Enum and is_attr: + raise TypeError(f"{tango_resource} has type Enum not {datatype.__name__}") + if tr_dtype == Enum and is_attr: choices = tuple(v.name for v in datatype) if set(choices) != set(trl_choices): - raise TypeError(f"{trl} has choices {trl_choices} not {choices}") - return dict(source=trl, dtype="string", shape=[], choices=trl_choices) + raise TypeError(f"{tango_resource} has choices {trl_choices} not {choices}") + return dict(source=tango_resource, dtype="string", shape=[], choices=trl_choices) else: - if datatype and not issubclass(trl_dtype, datatype): - raise TypeError(f"{trl} has type {trl_dtype.__name__} not {datatype.__name__}") - return dict(source=trl, dtype=trl_dtype_desc, shape=[]) + if datatype and not issubclass(tr_dtype, datatype): + raise TypeError(f"{tango_resource} has type {tr_dtype.__name__} not {datatype.__name__}") + return dict(source=tango_resource, dtype=tr_dtype_desc, shape=[]) # -------------------------------------------------------------------- @@ -389,4 +398,4 @@ def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: else: if self.eid: self.proxies[self.read_trl].unsubscribe_callback(self.eid) - self.eid = None \ No newline at end of file + self.eid = None diff --git a/src/ophyd_async/tango/device/__init__.py b/src/ophyd_async/tango/device/__init__.py index 9873cbc16c..a83016b7db 100644 --- a/src/ophyd_async/tango/device/__init__.py +++ b/src/ophyd_async/tango/device/__init__.py @@ -1,5 +1,3 @@ -from .device import (TangoDevice, TangoStandardReadableDevice, - ReadableSignal, ReadableUncachedSignal, ConfigurableSignal) +from .device import TangoReadableDevice -__all__ = ("TangoDevice", "TangoStandardReadableDevice", - "ReadableSignal", "ReadableUncachedSignal", "ConfigurableSignal") +__all__ = ("TangoReadableDevice",) diff --git a/src/ophyd_async/tango/device/device.py b/src/ophyd_async/tango/device/device.py index 7432a501dd..882690e4cc 100644 --- a/src/ophyd_async/tango/device/device.py +++ b/src/ophyd_async/tango/device/device.py @@ -2,87 +2,18 @@ from __future__ import annotations -from inspect import isclass -from typing import get_type_hints, Union, Generic, Tuple, Sequence, Type, get_origin, get_args +from abc import abstractmethod +from typing import Union, Tuple, Sequence -from tango import AttrWriteType, CmdArgType from tango.asyncio import DeviceProxy -from ophyd_async.core import StandardReadable, SignalW, SignalX, SignalR, SignalRW, Signal, T -from ophyd_async.tango.signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x +from ophyd_async.core import StandardReadable -__all__ = ("TangoDevice", - "TangoStandardReadableDevice", - "ReadableSignal", - "ReadableUncachedSignal", - "ConfigurableSignal") +__all__ = ("TangoReadableDevice", ) # -------------------------------------------------------------------- - -class _AutoSignal(Signal): - """ - To mark attribute/command in devices type hits as the one, which has to be read every step - """ - - -# -------------------------------------------------------------------- -class ReadableSignal(_AutoSignal, Generic[T]): - """ - To mark attribute/command in devices type hits as the one, which has to be read every step - """ - - _add_me_to = "_read_signals" - - -# -------------------------------------------------------------------- -class ReadableUncachedSignal(_AutoSignal, Generic[T]): - """ - To mark attribute/command in devices type hits as the one, which has to be read every step - """ - - _add_me_to = "_read_uncached_signals" - - -# -------------------------------------------------------------------- -class ConfigurableSignal(_AutoSignal, Generic[T]): - """ - To mark attribute/command in devices type hits as the one, which has to be read only as startup - """ - - _add_me_to = "_configuration_signals" - - -# -------------------------------------------------------------------- -def get_signal(dtype: Type[T], name: str, trl: str, device_proxy: DeviceProxy) -> Union[SignalW, SignalX, SignalR, SignalRW]: - ftrl = trl + '/' + name - if name in device_proxy.get_attribute_list(): - conf = device_proxy.get_attribute_config(name, green_mode=False) - - if conf.writable in [AttrWriteType.READ_WRITE, AttrWriteType.READ_WITH_WRITE]: - return tango_signal_rw(dtype, ftrl, ftrl, device_proxy) - elif conf.writable == AttrWriteType.READ: - return tango_signal_r(dtype, ftrl, device_proxy) - else: - return tango_signal_w(dtype, ftrl, device_proxy) - if name in device_proxy.get_command_list(): - # TODO: check logic - conf = device_proxy.get_command_config(name, green_mode=False) - if conf.in_type == CmdArgType.DevVoid: - return tango_signal_x(ftrl, device_proxy) - elif conf.out_type != CmdArgType.DevVoid: - return tango_signal_rw(dtype, ftrl, ftrl, device_proxy) - else: - return tango_signal_r(dtype, ftrl, device_proxy) - - if name in device_proxy.get_pipe_list(): - raise NotImplemented("Pipes are not supported") - - raise RuntimeError(f"{name} cannot be found in {device_proxy.name()}") - - -# -------------------------------------------------------------------- -class TangoDevice: +class TangoReadableDevice(StandardReadable): """ General class for TangoDevices @@ -92,32 +23,15 @@ class TangoDevice: """ # -------------------------------------------------------------------- - def __init__(self, trl: str) -> None: + def __init__(self, trl: str, name="") -> None: self.trl = trl self.proxy: DeviceProxy = None + StandardReadable.__init__(self, name=name) # -------------------------------------------------------------------- def __await__(self): async def closure(): self.proxy = await DeviceProxy(self.trl) - hints = get_type_hints(self) - signals = {} - for name, dtype in hints.items(): - add_to = None - origin = get_origin(dtype) - if isclass(origin) and issubclass(origin, _AutoSignal): - add_to = origin._add_me_to - dtype = get_args(dtype)[0] - signal = get_signal(dtype, name, self.trl, self.proxy) # type: ignore - setattr(self, name, signal) - if add_to: - if add_to not in signals: - signals[add_to] = [getattr(self, name)] - else: - signals[add_to].append(getattr(self, name)) - for name, signals_set in signals.items(): - setattr(self, name, tuple(signals_set)) - self.register_signals() return self @@ -125,42 +39,8 @@ async def closure(): return closure().__await__() # -------------------------------------------------------------------- + @abstractmethod def register_signals(self): """ This method can be used to manually register signals """ - - -# -------------------------------------------------------------------- -class TangoStandardReadableDevice(TangoDevice, StandardReadable): - - _read_signals: Tuple[Union[SignalR, SignalRW], ...] = () - _configuration_signals: Tuple[Union[SignalR, SignalRW], ...] = () - _read_uncached_signals: Tuple[Union[SignalR, SignalRW], ...] = () - - # -------------------------------------------------------------------- - def __init__(self, trl: str, name="") -> None: - TangoDevice.__init__(self, trl) - StandardReadable.__init__(self, name=name) - - # -------------------------------------------------------------------- - def set_readable_signals( - self, - read: Sequence[Union[SignalR, SignalRW]] = (), - config: Sequence[Union[SignalR, SignalRW]] = (), - read_uncached: Sequence[Union[SignalR, SignalRW]] = (), - ): - """ - Parameters - ---------- - read: - Signals to make up :meth:`~StandardReadable.read` - config: - Signals to make up :meth:`~StandardReadable.read_configuration` - read_uncached: - Signals to make up :meth:`~StandardReadable.read` that won't be cached - """ - self._read_signals = tuple(read) - self._configuration_signals = tuple(config) - self._read_uncached_signals = tuple(read_uncached) - diff --git a/src/ophyd_async/tango/sardana/__init__.py b/src/ophyd_async/tango/sardana/__init__.py new file mode 100644 index 0000000000..3c77828dc9 --- /dev/null +++ b/src/ophyd_async/tango/sardana/__init__.py @@ -0,0 +1,3 @@ +from ophyd_async.tango.sardana.devices import SardanaMotor + +__all__ = ("SardanaMotor",) \ No newline at end of file diff --git a/src/ophyd_async/tango/sardana/devices.py b/src/ophyd_async/tango/sardana/devices.py new file mode 100644 index 0000000000..7c5adb9229 --- /dev/null +++ b/src/ophyd_async/tango/sardana/devices.py @@ -0,0 +1,87 @@ + +from __future__ import annotations + +import asyncio +import time + +from typing import Optional, List, Callable + +from bluesky.protocols import Locatable, Stoppable, Location + +from tango import DevState + +from ophyd_async.tango import TangoReadableDevice, tango_signal_x, tango_signal_r, tango_signal_rw, tango_signal_w +from ophyd_async.core import AsyncStatus, Signal + + +# -------------------------------------------------------------------- +class SardanaMotor(TangoReadableDevice, Locatable, Stoppable): + + # -------------------------------------------------------------------- + def __init__(self, trl: str, name="") -> None: + TangoReadableDevice.__init__(self, trl, name) + self._set_success = True + + # -------------------------------------------------------------------- + def register_signals(self): + + self.position = tango_signal_rw(float, self.trl + '/Position', device_proxy=self.proxy) + self.baserate = tango_signal_rw(float, self.trl + '/Base_rate', device_proxy=self.proxy) + self.velocity = tango_signal_rw(float, self.trl + '/Velocity', device_proxy=self.proxy) + self.acceleration = tango_signal_rw(float, self.trl + '/Acceleration', device_proxy=self.proxy) + self.deceleration = tango_signal_rw(float, self.trl + '/Deceleration', device_proxy=self.proxy) + + self.set_readable_signals(read=[self.position], + config=[self.baserate, + self.velocity, + self.acceleration, + self.deceleration]) + + self._stop = tango_signal_x(self.trl + '/Stop', self.proxy) + self._state = tango_signal_r(DevState, self.trl + '/State', self.proxy) + + # -------------------------------------------------------------------- + async def _move(self, new_position: float, watchers: List[Callable] = []): + self._set_success = True + start = time.monotonic() + start_position = await self.position.get_value() + + def update_watchers(current_position: float): + for watcher in watchers: + watcher( + name=self.name, + current=current_position, + initial=start_position, + target=new_position, + time_elapsed=time.monotonic() - start, + ) + + self.position.subscribe_value(update_watchers) + try: + await self.position.set(new_position) + await asyncio.sleep(0.1) + while await self._state.get_value() == DevState.MOVING: + print("Waiting for motor") + await asyncio.sleep(0.1) + finally: + self.position.clear_sub(update_watchers) + if not self._set_success: + raise RuntimeError("Motor was stopped") + + # -------------------------------------------------------------------- + def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus: + watchers: List[Callable] = [] + coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout) + return AsyncStatus(coro, watchers) + + # -------------------------------------------------------------------- + async def locate(self) -> Location: + pass + + # -------------------------------------------------------------------- + async def stop(self, success=False): + self._set_success = success + # Put with completion will never complete as we are waiting for completion on + # the move above, so need to pass wait=False + await self._stop.execute(wait=False) + diff --git a/src/ophyd_async/tango/signal/__init__.py b/src/ophyd_async/tango/signal/__init__.py index df44991efa..d37672efdc 100644 --- a/src/ophyd_async/tango/signal/__init__.py +++ b/src/ophyd_async/tango/signal/__init__.py @@ -1,8 +1,9 @@ -from .signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x +from .signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x, tango_signal_auto __all__ = ( "tango_signal_r", "tango_signal_rw", "tango_signal_w", - "tango_signal_x" + "tango_signal_x", + "tango_signal_auto" ) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 179a7b1daa..49abde98d1 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -2,8 +2,11 @@ from __future__ import annotations -from typing import Optional, Type -from tango import DeviceProxy +from typing import Optional, Type, Union +from tango.asyncio import DeviceProxy +from tango import DeviceProxy as SyncDeviceProxy +from tango import AttrWriteType, CmdArgType + from ophyd_async.core import SignalR, SignalX, T @@ -12,7 +15,8 @@ __all__ = ("tango_signal_rw", "tango_signal_r", "tango_signal_w", - "tango_signal_x") + "tango_signal_x", + "tango_signal_auto") # -------------------------------------------------------------------- @@ -63,7 +67,7 @@ def tango_signal_w(datatype: Type[T], write_trl: str, device_proxy: Optional[DeviceProxy] = None ) -> TangoSignalW[T]: - """Create a `SignalW` backed by 1 Tango Attribute/Command + """Create a `TangoSignalW` backed by 1 Tango Attribute/Command Parameters ---------- @@ -93,3 +97,34 @@ def tango_signal_x(write_trl: str, """ backend: TangoSignalBackend = TangoTransport(None, write_trl, write_trl, device_proxy) return SignalX(backend) + + +# -------------------------------------------------------------------- +def tango_signal_auto(datatype: Type[T], full_trl: str, device_proxy: Optional[DeviceProxy] = None) -> \ + Union[TangoSignalW, SignalX, SignalR, TangoSignalRW]: + backend: TangoSignalBackend = TangoTransport(datatype, full_trl, full_trl, device_proxy) + + device_trl, tr_name = full_trl.rsplit('/', 1) + device_proxy = SyncDeviceProxy(device_trl) + if tr_name in device_proxy.get_attribute_list(): + config = device_proxy.get_attribute_config(tr_name) + if config.writable in [AttrWriteType.READ_WRITE, AttrWriteType.READ_WITH_WRITE]: + return TangoSignalRW(backend) + elif config.writable == AttrWriteType.READ: + return SignalR(backend) + else: + return TangoSignalW(backend) + + if tr_name in device_proxy.get_command_list(): + config = device_proxy.get_command_config(tr_name) + if config.in_type == CmdArgType.DevVoid: + return SignalX(backend) + elif config.out_type != CmdArgType.DevVoid: + return TangoSignalRW(backend) + else: + return SignalR(backend) + + if tr_name in device_proxy.get_pipe_list(): + raise NotImplemented("Pipes are not supported") + + raise RuntimeError(f"Cannot find {tr_name} in {device_trl}") diff --git a/tests/tango/test_device.py b/tests/tango/test_device.py index ff6045f578..a3687945bf 100644 --- a/tests/tango/test_device.py +++ b/tests/tango/test_device.py @@ -2,11 +2,10 @@ import pytest -from typing import get_type_hints, Any, Optional, Tuple, Type +from typing import Type from enum import Enum, IntEnum import numpy as np -import numpy.typing as npt from tango import AttrQuality, AttrDataFormat, AttrWriteType, DeviceProxy, DevState, CmdArgType from tango.test_utils import assert_close @@ -15,9 +14,12 @@ from tango.test_context import MultiDeviceTestContext from ophyd_async.tango._backend._tango_transport import get_pyton_type -from ophyd_async.tango.device import TangoStandardReadableDevice, ReadableSignal +from ophyd_async.tango import TangoReadableDevice, tango_signal_auto from ophyd_async.core import DeviceCollector, T +from bluesky import RunEngine +from bluesky.plans import count + class TestEnum(IntEnum): __test__ = False @@ -28,6 +30,10 @@ class TestEnum(IntEnum): # -------------------------------------------------------------------- # fixtures to run Echo device # -------------------------------------------------------------------- + +TESTED_FEATURES = ["array", "limitedvalue", "justvalue"] + + class TestDevice(Device): __test__ = False @@ -59,50 +65,24 @@ def write_limitedvalue(self, value: float): # -------------------------------------------------------------------- -def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: - if python_type in [bool, int]: - return dict(dtype="integer", shape=[]) - if python_type in [float]: - return dict(dtype="number", shape=[]) - if python_type in [str]: - return dict(dtype="string", shape=[]) - if issubclass(python_type, DevState): - return dict(dtype="string", shape=[], choices=list(DevState.names.keys())) - if issubclass(python_type, Enum): - return dict(dtype="string", shape=[], choices=[] if is_cmd else [member.name for member in value.__class__]) - - return dict(dtype="array", shape=[np.Inf] if is_cmd else list(np.array(value).shape)) - - -# -------------------------------------------------------------------- -@pytest.fixture(scope="module") -def tango_test_device(): - with MultiDeviceTestContext( - [{"class": TestDevice, "devices": [{"name": "test/device/1"}]}], process=True) as context: - yield context.get_device_access("test/device/1") - - -# -------------------------------------------------------------------- -@pytest.fixture(autouse=True) -def reset_tango_asyncio(): - set_global_executor(None) +class TestReadableDevice(TangoReadableDevice): + __test__ = False + # -------------------------------------------------------------------- + def register_signals(self): + for name in TESTED_FEATURES: + setattr(self, name, tango_signal_auto(None, f"{self.trl}/{name}", self.proxy)) -# -------------------------------------------------------------------- -class TestReadableDevice(TangoStandardReadableDevice): - __test__ = False - justvalue: ReadableSignal[int] - array: ReadableSignal[npt.NDArray[float]] - limitedvalue: ReadableSignal[float] + self.set_readable_signals(read_uncached=[getattr(self, name) for name in TESTED_FEATURES]) # -------------------------------------------------------------------- -def describe_class(cls, fqtrl): +def describe_class(fqtrl): description = {} values = {} dev = DeviceProxy(fqtrl) - hints = get_type_hints(cls) - for name, dtype in hints.items(): + + for name in TESTED_FEATURES: if name in dev.get_attribute_list(): attr_conf = dev.get_attribute_config(name) value = dev.read_attribute(name).value @@ -122,12 +102,12 @@ def describe_class(cls, fqtrl): elif name in dev.get_command_list(): cmd_conf = dev.get_command_config(name) _, _, descr = get_pyton_type(cmd_conf.in_type if cmd_conf.in_type != CmdArgType.DevVoid else cmd_conf.out_type) - shape = [] is_array = False + shape = [] value = getattr(dev, name)() else: - raise RuntimeError(f"{name} cannot be found in {cls} attributes/commands") + raise RuntimeError(f"Cannot find {name} in attributes/commands (pipes are not supported!)") description[f"test_device-{name}"] = {'source': f'{fqtrl}/{name}', # type: ignore 'dtype': 'array' if is_array else descr, @@ -140,6 +120,36 @@ def describe_class(cls, fqtrl): return values, description +# -------------------------------------------------------------------- +def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: + if python_type in [bool, int]: + return dict(dtype="integer", shape=[]) + if python_type in [float]: + return dict(dtype="number", shape=[]) + if python_type in [str]: + return dict(dtype="string", shape=[]) + if issubclass(python_type, DevState): + return dict(dtype="string", shape=[], choices=list(DevState.names.keys())) + if issubclass(python_type, Enum): + return dict(dtype="string", shape=[], choices=[] if is_cmd else [member.name for member in value.__class__]) + + return dict(dtype="array", shape=[np.Inf] if is_cmd else list(np.array(value).shape)) + + +# -------------------------------------------------------------------- +@pytest.fixture(scope="module") +def tango_test_device(): + with MultiDeviceTestContext( + [{"class": TestDevice, "devices": [{"name": "test/device/1"}]}], process=True) as context: + yield context.get_device_access("test/device/1") + + +# -------------------------------------------------------------------- +@pytest.fixture(autouse=True) +def reset_tango_asyncio(): + set_global_executor(None) + + # -------------------------------------------------------------------- def compare_values(expected, received): assert set(list(expected.keys())) == set(list(received.keys())) @@ -151,7 +161,7 @@ def compare_values(expected, received): # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_connect(tango_test_device): - values, description = describe_class(TestReadableDevice(""), tango_test_device) + values, description = describe_class(tango_test_device) async with DeviceCollector(): test_device = await TestReadableDevice(tango_test_device) @@ -159,3 +169,14 @@ async def test_connect(tango_test_device): assert test_device.name == "test_device" assert description == await test_device.describe() compare_values(values, await test_device.read()) + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_with_bluesky(tango_test_device): + async with DeviceCollector(): + ophyd_dev = await TestReadableDevice(tango_test_device) + + # now let's do some bluesky stuff + RE = RunEngine() + RE(count([ophyd_dev], 1)) \ No newline at end of file diff --git a/tests/tango/test_sardana.py b/tests/tango/test_sardana.py new file mode 100644 index 0000000000..19f5ec02c3 --- /dev/null +++ b/tests/tango/test_sardana.py @@ -0,0 +1,22 @@ +import pytest + +from tango.asyncio_executor import set_global_executor + +from ophyd_async.tango.sardana import SardanaMotor +from ophyd_async.core import DeviceCollector + + +# -------------------------------------------------------------------- +@pytest.fixture(autouse=True) +def reset_tango_asyncio(): + set_global_executor(None) + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_connect(): + + async with DeviceCollector(): + dummy_motor = await SardanaMotor("motor/dummy_mot_ctrl/1") + + assert dummy_motor.name == "dummy_motor" diff --git a/tests/tango/test_signals.py b/tests/tango/test_signals.py index f37a1a621f..9ec1dd0464 100644 --- a/tests/tango/test_signals.py +++ b/tests/tango/test_signals.py @@ -190,13 +190,15 @@ def prepare_device(echo_device: str, pv: str, put_value: T) -> None: # -------------------------------------------------------------------- class MonitorQueue: def __init__(self, backend: SignalBackend): + self.updates: asyncio.Queue[Tuple[Reading, Any]] = asyncio.Queue() self.backend = backend self.subscription = backend.set_callback(self.add_reading_value) - self.updates: asyncio.Queue[Tuple[Reading, Any]] = asyncio.Queue() + # -------------------------------------------------------------------- def add_reading_value(self, reading: Reading, value): self.updates.put_nowait((reading, value)) + # -------------------------------------------------------------------- async def assert_updates(self, expected_value): expected_reading = { "timestamp": pytest.approx(time.time(), rel=0.1), @@ -217,6 +219,7 @@ async def assert_updates(self, expected_value): assert_close(update_value, expected_value) assert_close(get_value, expected_value) + # -------------------------------------------------------------------- def close(self): self.backend.set_callback(None) @@ -258,12 +261,11 @@ async def test_backend_get_put_monitor_attr(echo_device: str, py_type: Type[T], initial_value: T, put_value: T): - # With the given datatype, check we have the correct initial value and putting - # works + # With the given datatype, check we have the correct initial value and putting works descriptor = get_test_descriptor(py_type, initial_value, False) await assert_monitor_then_put(echo_device, pv, initial_value, put_value, descriptor, py_type) - # # With datatype guessed from CA/PVA, check we can set it back to the initial value - await assert_monitor_then_put(echo_device, pv, initial_value, put_value, descriptor) + # # With guessed datatype, check we can set it back to the initial value + # await assert_monitor_then_put(echo_device, pv, initial_value, put_value, descriptor) # -------------------------------------------------------------------- @@ -304,9 +306,9 @@ async def test_backend_get_put_monitor_cmd(echo_device: str, py_type: Type[T], initial_value: T, put_value: T): - # With the given datatype, check we have the correct initial value and putting - # works + print("Starting test!") + # With the given datatype, check we have the correct initial value and putting works descriptor = get_test_descriptor(py_type, initial_value, True) await assert_put_read(echo_device, pv, put_value, descriptor, py_type) - # # With datatype guessed from CA/PVA, check we can set it back to the initial value + # # With guessed datatype, check we can set it back to the initial value await assert_put_read(echo_device, pv, put_value, descriptor) \ No newline at end of file From 26ccd049334904b8cba0836ab9eb9b97ee871c27 Mon Sep 17 00:00:00 2001 From: matveyev Date: Mon, 6 Nov 2023 15:18:56 +0100 Subject: [PATCH 008/141] Sardana motor working --- .../tango/_backend/_tango_transport.py | 65 ++++++++------ src/ophyd_async/tango/device/device.py | 3 +- src/ophyd_async/tango/sardana/devices.py | 3 +- tests/tango/test_device.py | 2 +- tests/tango/test_sardana.py | 89 ++++++++++++++++++- 5 files changed, 128 insertions(+), 34 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 0eb6bdf845..ceda459daf 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -1,7 +1,6 @@ import asyncio import time import functools -import logging import numpy as np from asyncio import CancelledError @@ -34,6 +33,9 @@ __all__ = ("TangoTransport", "TangoSignalBackend") +# time constant to wait for timeout +A_BIT = 1e-5 + # -------------------------------------------------------------------- def ensure_proper_executor(func): @@ -129,7 +131,7 @@ def subscribe_callback(self, callback: Optional[ReadingValueCallback]): # -------------------------------------------------------------------- @abstractmethod - def unsubscribe_callback(self, callback: Optional[ReadingValueCallback]): + def unsubscribe_callback(self): """delete CHANGE event subscription""" @@ -156,14 +158,18 @@ async def get_w_value(self) -> T: @ensure_proper_executor async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None) -> None: if wait: - if timeout: - rid = self._proxy.write_attribute_asynch(self._name, value, green_mode=GreenMode.Synchronous) - await asyncio.sleep(timeout) - self._proxy.write_attribute_reply(rid, green_mode=GreenMode.Synchronous) - else: - self._proxy.write_attribute(self._name, value, green_mode=GreenMode.Synchronous) - else: await self._proxy.write_attribute(self._name, value) + else: + rid = await self._proxy.write_attribute_asynch(self._name, value) + if timeout: + finished = False + while not finished: + try: + val = await dev.write_attribute_reply(rid) + finished = True + except: + await asyncio.sleep(A_BIT) + # -------------------------------------------------------------------- @ensure_proper_executor @@ -188,13 +194,15 @@ def has_subscription(self) -> bool: def subscribe_callback(self, callback: Optional[ReadingValueCallback]): """add user callback to CHANGE event subscription""" self._event_callback = callback - self._eid = self._proxy.subscribe_event(self._name, EventType.CHANGE_EVENT, self._event_processor, - green_mode=False) + if not self._eid: + self._eid = self._proxy.subscribe_event(self._name, EventType.CHANGE_EVENT, self._event_processor, + green_mode=False) # -------------------------------------------------------------------- - def unsubscribe_callback(self, eid: int): - self._proxy.unsubscribe_event(self._eid, green_mode=False) - self._eid = None + def unsubscribe_callback(self): + if self._eid: + self._proxy.unsubscribe_event(self._eid, green_mode=False) + self._eid = None self._event_callback = None # -------------------------------------------------------------------- @@ -222,14 +230,18 @@ async def get(self) -> T: # @ensure_proper_executor async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None) -> None: if wait: - if timeout: - rid = self._proxy.command_inout_asynch(self._name, value, green_mode=GreenMode.Synchronous) - await asyncio.sleep(timeout) - val = self._proxy.command_inout_reply(rid, green_mode=GreenMode.Synchronous) - else: - val = self._proxy.command_inout(self._name, value, green_mode=GreenMode.Synchronous) - else: val = await self._proxy.command_inout(self._name, value) + else: + val = None + rid = await self._proxy.command_inout_asynch(self._name, value) + if timeout: + finished = False + while not finished: + try: + val = await dev.command_inout_reply(rid) + finished = True + except: + await asyncio.sleep(A_BIT) self._last_reading = dict(value=val, timestamp=time.time(), alarm_severity=0) @@ -344,7 +356,6 @@ def __init__(self, self.trl_configs: Dict[str, AttributeInfoEx] = {} self.source = f"{self.read_trl}" self.descriptor: Descriptor = {} # type: ignore - self.eid: Optional[int] = None # -------------------------------------------------------------------- async def _connect_and_store_config(self, trl): @@ -393,9 +404,11 @@ def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: if callback: assert (not self.proxies[self.read_trl].has_subscription()), "Cannot set a callback when one is already set" - self.eid = self.proxies[self.read_trl].subscribe_callback(callback) + try: + self.proxies[self.read_trl].subscribe_callback(callback) + except Exception as err: + raise RuntimeError(f"Cannot set event for {self.read_trl}. " + f"This signal should be used only as non-cached!") else: - if self.eid: - self.proxies[self.read_trl].unsubscribe_callback(self.eid) - self.eid = None + self.proxies[self.read_trl].unsubscribe_callback() diff --git a/src/ophyd_async/tango/device/device.py b/src/ophyd_async/tango/device/device.py index 882690e4cc..f2415edd7a 100644 --- a/src/ophyd_async/tango/device/device.py +++ b/src/ophyd_async/tango/device/device.py @@ -3,7 +3,6 @@ from __future__ import annotations from abc import abstractmethod -from typing import Union, Tuple, Sequence from tango.asyncio import DeviceProxy @@ -42,5 +41,5 @@ async def closure(): @abstractmethod def register_signals(self): """ - This method can be used to manually register signals + This method should be used to register signals """ diff --git a/src/ophyd_async/tango/sardana/devices.py b/src/ophyd_async/tango/sardana/devices.py index 7c5adb9229..af90b770a7 100644 --- a/src/ophyd_async/tango/sardana/devices.py +++ b/src/ophyd_async/tango/sardana/devices.py @@ -31,7 +31,7 @@ def register_signals(self): self.acceleration = tango_signal_rw(float, self.trl + '/Acceleration', device_proxy=self.proxy) self.deceleration = tango_signal_rw(float, self.trl + '/Deceleration', device_proxy=self.proxy) - self.set_readable_signals(read=[self.position], + self.set_readable_signals(read_uncached=[self.position], config=[self.baserate, self.velocity, self.acceleration, @@ -61,7 +61,6 @@ def update_watchers(current_position: float): await self.position.set(new_position) await asyncio.sleep(0.1) while await self._state.get_value() == DevState.MOVING: - print("Waiting for motor") await asyncio.sleep(0.1) finally: self.position.clear_sub(update_watchers) diff --git a/tests/tango/test_device.py b/tests/tango/test_device.py index a3687945bf..59cef794c4 100644 --- a/tests/tango/test_device.py +++ b/tests/tango/test_device.py @@ -179,4 +179,4 @@ async def test_with_bluesky(tango_test_device): # now let's do some bluesky stuff RE = RunEngine() - RE(count([ophyd_dev], 1)) \ No newline at end of file + RE(count([ophyd_dev], 1)) diff --git a/tests/tango/test_sardana.py b/tests/tango/test_sardana.py index 19f5ec02c3..ccd15c5cd0 100644 --- a/tests/tango/test_sardana.py +++ b/tests/tango/test_sardana.py @@ -1,10 +1,23 @@ import pytest +import asyncio + +import numpy as np + +from unittest.mock import Mock, call from tango.asyncio_executor import set_global_executor from ophyd_async.tango.sardana import SardanaMotor from ophyd_async.core import DeviceCollector +from bluesky import RunEngine +from bluesky.plans import count, scan +from bluesky.plan_stubs import mv + + +# Long enough for multiple asyncio event loop cycles to run so +# all the tasks have a chance to run +A_BIT = 0.001 # -------------------------------------------------------------------- @pytest.fixture(autouse=True) @@ -13,10 +26,80 @@ def reset_tango_asyncio(): # -------------------------------------------------------------------- -@pytest.mark.asyncio -async def test_connect(): - +@pytest.fixture +async def dummy_motor(): async with DeviceCollector(): dummy_motor = await SardanaMotor("motor/dummy_mot_ctrl/1") + yield dummy_motor + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_connect(dummy_motor): + assert dummy_motor.name == "dummy_motor" + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_readout_with_bluesky(dummy_motor): + + # now let's do some bluesky stuff + RE = RunEngine() + RE(count([dummy_motor], 1)) + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_move(dummy_motor): + start_position = await dummy_motor.position.get_value() + target_position = start_position + 1 + + status = dummy_motor.set(target_position) + watcher = Mock() + status.watch(watcher) + done = Mock() + status.add_callback(done) + await asyncio.sleep(A_BIT) + assert watcher.call_count == 1 + assert watcher.call_args == call( + name="dummy_motor", + current=start_position, + initial=start_position, + target=target_position, + time_elapsed=pytest.approx(0.0, abs=0.05), + ) + await status + assert pytest.approx(target_position, abs=0.1) == await dummy_motor.position.get_value() + assert status.done + done.assert_called_once_with(status) + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_move_with_bluesky(dummy_motor): + start_position = await dummy_motor.position.get_value() + target_position = start_position + 1 + + # now let's do some bluesky stuff + RE = RunEngine() + RE(mv(dummy_motor, target_position)) + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_scan_motor_vs_motor_position(dummy_motor): + + readouts = Mock() + + # now let's do some bluesky stuff + RE = RunEngine() + RE(scan([dummy_motor.position], dummy_motor, 0, 1, num=11), readouts) + + assert readouts.call_count == 14 + assert set([args[0][0] for args in readouts.call_args_list]) == {'descriptor', 'event', 'start', 'stop'} + + positions = [args[0][1]['data']['dummy_motor-position'] for args in readouts.call_args_list if args[0][0] == "event"] + for got, expected in zip(positions, np.arange(0, 1.1, 0.1)): + assert pytest.approx(got, abs=0.1) == expected \ No newline at end of file From cf2decbe9eaaaf1b89e267f5e93614e03aaaf8f3 Mon Sep 17 00:00:00 2001 From: matveyev Date: Mon, 15 Jan 2024 17:55:22 +0100 Subject: [PATCH 009/141] added OmsVME58, DGG2, SIS3820 devices and tests --- src/ophyd_async/core/flyer.py | 4 +- src/ophyd_async/tango/__init__.py | 2 +- src/ophyd_async/tango/_backend/__init__.py | 9 +- .../tango/_backend/_signal_backend.py | 26 ++++- .../tango/_backend/_tango_transport.py | 45 +++++++-- .../tango/base_devices/__init__.py | 3 + .../device.py => base_devices/base_device.py} | 16 +++- src/ophyd_async/tango/device/__init__.py | 3 - .../tango/device_controllers/__init__.py | 7 ++ .../tango/device_controllers/dgg2.py | 70 ++++++++++++++ .../tango/device_controllers/omsvme58.py | 94 +++++++++++++++++++ .../tango/device_controllers/sis3820.py | 40 ++++++++ src/ophyd_async/tango/sardana/__init__.py | 2 +- .../tango/sardana/{devices.py => motor.py} | 1 + src/ophyd_async/tango/signal/signal.py | 21 +++-- .../{test_device.py => test_base_device.py} | 0 tests/tango/test_ddg2.py | 55 +++++++++++ .../tango/{test_sardana.py => test_motors.py} | 21 ++++- tests/tango/test_scans.py | 55 +++++++++++ tests/tango/test_sis3820.py | 52 ++++++++++ 20 files changed, 491 insertions(+), 35 deletions(-) create mode 100644 src/ophyd_async/tango/base_devices/__init__.py rename src/ophyd_async/tango/{device/device.py => base_devices/base_device.py} (63%) delete mode 100644 src/ophyd_async/tango/device/__init__.py create mode 100644 src/ophyd_async/tango/device_controllers/__init__.py create mode 100644 src/ophyd_async/tango/device_controllers/dgg2.py create mode 100644 src/ophyd_async/tango/device_controllers/omsvme58.py create mode 100644 src/ophyd_async/tango/device_controllers/sis3820.py rename src/ophyd_async/tango/sardana/{devices.py => motor.py} (98%) rename tests/tango/{test_device.py => test_base_device.py} (100%) create mode 100644 tests/tango/test_ddg2.py rename tests/tango/{test_sardana.py => test_motors.py} (85%) create mode 100644 tests/tango/test_scans.py create mode 100644 tests/tango/test_sis3820.py diff --git a/src/ophyd_async/core/flyer.py b/src/ophyd_async/core/flyer.py index 86d368568b..0d46c7a006 100644 --- a/src/ophyd_async/core/flyer.py +++ b/src/ophyd_async/core/flyer.py @@ -20,7 +20,7 @@ Flyable, HasHints, Hints, - Preparable, + # Preparable, Reading, Stageable, WritesExternalAssets, @@ -174,7 +174,7 @@ async def stop(self): class HardwareTriggeredFlyable( Device, - Preparable, + # Preparable, Stageable, Flyable, # Collectable, diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py index 338ad1a455..f52d66a165 100644 --- a/src/ophyd_async/tango/__init__.py +++ b/src/ophyd_async/tango/__init__.py @@ -1,5 +1,5 @@ from ophyd_async.tango.signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x, tango_signal_auto -from ophyd_async.tango.device import TangoReadableDevice +from ophyd_async.tango.base_devices import TangoReadableDevice __all__ = [ "tango_signal_r", diff --git a/src/ophyd_async/tango/_backend/__init__.py b/src/ophyd_async/tango/_backend/__init__.py index 553e38db6c..5f55d915be 100644 --- a/src/ophyd_async/tango/_backend/__init__.py +++ b/src/ophyd_async/tango/_backend/__init__.py @@ -1,4 +1,9 @@ from ophyd_async.tango._backend._tango_transport import TangoTransport, TangoSignalBackend -from ophyd_async.tango._backend._signal_backend import TangoSignalW, TangoSignalRW +from ophyd_async.tango._backend._signal_backend import TangoSignalW, TangoSignalRW, TangoSignalR, TangoSignalX -__all__ = ("TangoTransport", "TangoSignalBackend", "TangoSignalW", "TangoSignalRW") \ No newline at end of file +__all__ = ("TangoTransport", + "TangoSignalBackend", + "TangoSignalW", + "TangoSignalRW", + "TangoSignalR", + "TangoSignalX") \ No newline at end of file diff --git a/src/ophyd_async/tango/_backend/_signal_backend.py b/src/ophyd_async/tango/_backend/_signal_backend.py index e5a0357cd5..bc17fa1fb6 100644 --- a/src/ophyd_async/tango/_backend/_signal_backend.py +++ b/src/ophyd_async/tango/_backend/_signal_backend.py @@ -2,12 +2,12 @@ from typing import Optional -from ophyd_async.core import T, SignalW, SignalRW +from ophyd_async.core import T, SignalW, SignalRW, SignalR, SignalX from ophyd_async.core.signal import add_timeout # -------------------------------------------------------------------- -# from tango attributes one can get setvalue, so we extend SignalRW and SignalW to add it +# from tango attributes one can get setvalue, so we extend SignalRW and SignalW class SignalWithSetpoit: @add_timeout async def get_setpoint(self, cached: Optional[bool] = None) -> T: @@ -16,10 +16,28 @@ async def get_setpoint(self, cached: Optional[bool] = None) -> T: # -------------------------------------------------------------------- -class TangoSignalW(SignalW[T], SignalWithSetpoit): +# not every tango attribute is configured to generate signals +class CachableOrNot: + def is_cachable(self) -> T: + """The last written value to TRL""" + return self._backend.is_cachable() + + +# -------------------------------------------------------------------- +class TangoSignalW(SignalW[T], CachableOrNot, SignalWithSetpoit): + ... + + +# -------------------------------------------------------------------- +class TangoSignalRW(SignalRW[T], CachableOrNot, SignalWithSetpoit): + ... + + +# -------------------------------------------------------------------- +class TangoSignalR(SignalR[T], CachableOrNot): ... # -------------------------------------------------------------------- -class TangoSignalRW(SignalRW[T], SignalWithSetpoit): +class TangoSignalX(SignalX, CachableOrNot): ... diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index ceda459daf..33680d884b 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -95,6 +95,10 @@ def __init__(self, device_proxy: DeviceProxy, name: str): self._proxy = device_proxy self._name = name + # -------------------------------------------------------------------- + async def connect(self): + """perform actions after proxy is connected, e.g. checks if signal can be subscribed""" + # -------------------------------------------------------------------- @abstractmethod async def get(self) -> T: @@ -138,10 +142,19 @@ def unsubscribe_callback(self): # -------------------------------------------------------------------- class AttributeProxy(TangoProxy): - support_events = True + support_events = False _event_callback = None _eid = None + # -------------------------------------------------------------------- + async def connect(self) -> None: + try: + eid = await self._proxy.subscribe_event(self._name, EventType.CHANGE_EVENT, self._event_processor) + await self._proxy.unsubscribe_event(eid) + self.support_events = True + except Exception: + pass + # -------------------------------------------------------------------- @ensure_proper_executor async def get(self) -> T: @@ -165,12 +178,11 @@ async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[flo finished = False while not finished: try: - val = await dev.write_attribute_reply(rid) + _ = await self._proxy.write_attribute_reply(rid) finished = True except: await asyncio.sleep(A_BIT) - # -------------------------------------------------------------------- @ensure_proper_executor async def get_config(self) -> AttributeInfoEx: @@ -227,7 +239,7 @@ async def get(self) -> T: return self._last_reading["value"] # -------------------------------------------------------------------- - # @ensure_proper_executor + @ensure_proper_executor async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None) -> None: if wait: val = await self._proxy.command_inout(self._name, value) @@ -238,7 +250,7 @@ async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[flo finished = False while not finished: try: - val = await dev.command_inout_reply(rid) + val = await self._proxy.command_inout_reply(rid) finished = True except: await asyncio.sleep(A_BIT) @@ -246,7 +258,7 @@ async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[flo self._last_reading = dict(value=val, timestamp=time.time(), alarm_severity=0) # -------------------------------------------------------------------- - # @ensure_proper_executor + @ensure_proper_executor async def get_config(self) -> CommandInfo: return await self._proxy.get_command_config(self._name) @@ -330,12 +342,22 @@ def get_trl_descriptor(datatype: Optional[Type], tango_resource: str, # -------------------------------------------------------------------- async def get_tango_trl(full_trl: str, device_proxy: Optional[DeviceProxy]) -> TangoProxy: device_trl, trl_name = full_trl.rsplit('/', 1) + trl_name = trl_name.lower() device_proxy = device_proxy or await DeviceProxy(device_trl) - if trl_name in device_proxy.get_attribute_list(): + + # all attributes can be always accessible with low register + all_attrs = [attr_name.lower() for attr_name in device_proxy.get_attribute_list()] + if trl_name in all_attrs: return AttributeProxy(device_proxy, trl_name) - if trl_name in device_proxy.get_command_list(): + + # all commands can be always accessible with low register + all_cmds = [cmd_name.lower() for cmd_name in device_proxy.get_command_list()] + if trl_name in all_cmds: return CommandProxy(device_proxy, trl_name) - if trl_name in device_proxy.get_pipe_list(): + + # all pipes can be always accessible with low register + all_pipes = [pipe_name.lower() for pipe_name in device_proxy.get_pipe_list()] + if trl_name in all_pipes: raise NotImplemented("Pipes are not supported") raise RuntimeError(f"{trl_name} cannot be found in {device_proxy.name()}") @@ -361,6 +383,7 @@ def __init__(self, async def _connect_and_store_config(self, trl): try: self.proxies[trl] = await get_tango_trl(trl, self.proxies[trl]) + await self.proxies[trl].connect() self.trl_configs[trl] = await self.proxies[trl].get_config() except CancelledError: raise NotConnected(self.source) @@ -398,6 +421,10 @@ async def get_value(self) -> T: async def get_w_value(self) -> T: return await self.proxies[self.write_trl].get_w_value() + # -------------------------------------------------------------------- + def is_cachable(self): + return self.proxies[self.read_trl].support_events + # -------------------------------------------------------------------- def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: assert self.proxies[self.read_trl].support_events, f"{self.source} does not support events" diff --git a/src/ophyd_async/tango/base_devices/__init__.py b/src/ophyd_async/tango/base_devices/__init__.py new file mode 100644 index 0000000000..b46b306984 --- /dev/null +++ b/src/ophyd_async/tango/base_devices/__init__.py @@ -0,0 +1,3 @@ +from ophyd_async.tango.base_devices.base_device import TangoReadableDevice + +__all__ = ("TangoReadableDevice",) diff --git a/src/ophyd_async/tango/device/device.py b/src/ophyd_async/tango/base_devices/base_device.py similarity index 63% rename from src/ophyd_async/tango/device/device.py rename to src/ophyd_async/tango/base_devices/base_device.py index f2415edd7a..1cd17dfb72 100644 --- a/src/ophyd_async/tango/device/device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -6,7 +6,7 @@ from tango.asyncio import DeviceProxy -from ophyd_async.core import StandardReadable +from ophyd_async.core import StandardReadable, AsyncStatus __all__ = ("TangoReadableDevice", ) @@ -43,3 +43,17 @@ def register_signals(self): """ This method should be used to register signals """ + + # -------------------------------------------------------------------- + @AsyncStatus.wrap + async def stage(self) -> None: + for sig in self._read_signals + self._configuration_signals: + if hasattr(sig, "is_cachable") and sig.is_cachable(): + await sig.stage().task + + # -------------------------------------------------------------------- + @AsyncStatus.wrap + async def unstage(self) -> None: + for sig in self._read_signals + self._configuration_signals: + if hasattr(sig, "is_cachable") and sig.is_cachable(): + await sig.unstage().task diff --git a/src/ophyd_async/tango/device/__init__.py b/src/ophyd_async/tango/device/__init__.py deleted file mode 100644 index a83016b7db..0000000000 --- a/src/ophyd_async/tango/device/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .device import TangoReadableDevice - -__all__ = ("TangoReadableDevice",) diff --git a/src/ophyd_async/tango/device_controllers/__init__.py b/src/ophyd_async/tango/device_controllers/__init__.py new file mode 100644 index 0000000000..00cac79f68 --- /dev/null +++ b/src/ophyd_async/tango/device_controllers/__init__.py @@ -0,0 +1,7 @@ +from ophyd_async.tango.device_controllers.omsvme58 import OmsVME58Motor +from ophyd_async.tango.device_controllers.dgg2 import DGG2Timer +from ophyd_async.tango.device_controllers.sis3820 import SIS3820Counter + +__all__ = ("OmsVME58Motor", + "SIS3820Counter", + "DGG2Timer") diff --git a/src/ophyd_async/tango/device_controllers/dgg2.py b/src/ophyd_async/tango/device_controllers/dgg2.py new file mode 100644 index 0000000000..a70ce83fd6 --- /dev/null +++ b/src/ophyd_async/tango/device_controllers/dgg2.py @@ -0,0 +1,70 @@ + +from __future__ import annotations + +import asyncio +import time +from typing import List, Callable + +from bluesky.protocols import Triggerable + +from ophyd_async.core import AsyncStatus +from ophyd_async.tango import TangoReadableDevice, tango_signal_rw, tango_signal_x + + +# -------------------------------------------------------------------- +class DGG2Timer(TangoReadableDevice, Triggerable): + + # -------------------------------------------------------------------- + def __init__(self, trl: str, name="") -> None: + TangoReadableDevice.__init__(self, trl, name) + self._set_success = True + + # -------------------------------------------------------------------- + def register_signals(self): + + self.sampletime = tango_signal_rw(float, self.trl + '/sampletime', device_proxy=self.proxy) + self.remainingtime = tango_signal_rw(float, self.trl + '/remainingtime', device_proxy=self.proxy) + + self.set_readable_signals(read_uncached=[self.sampletime], + config=[self.sampletime]) + + self.startandwaitfortimer = tango_signal_x(self.trl+'/startandwaitfortimer', device_proxy=self.proxy) + + # -------------------------------------------------------------------- + async def _trigger(self, watchers: List[Callable] = []): + self._set_success = True + start = time.monotonic() + total_time = await self.sampletime.get_value() + + def update_watchers(remaining_time: float): + for watcher in watchers: + watcher( + name=self.name, + current=remaining_time, + initial=total_time, + target=total_time, + time_elapsed=time.monotonic() - start, + ) + + if self.remainingtime.is_cachable(): + self.remainingtime.subscribe_value(update_watchers) + else: + update_watchers(total_time) + try: + await self.startandwaitfortimer.trigger() + finally: + if self.remainingtime.is_cachable(): + self.remainingtime.clear_sub(update_watchers) + else: + update_watchers(await self.remainingtime.get_value()) + if not self._set_success: + raise RuntimeError("Timer was not triggered") + + # -------------------------------------------------------------------- + def trigger(self) -> AsyncStatus: + watchers: List[Callable] = [] + return AsyncStatus(self._trigger(watchers), watchers) + + # -------------------------------------------------------------------- + async def set_time(self, time: float) -> None: + await self.sampletime.set(time) diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py new file mode 100644 index 0000000000..e41a7403f5 --- /dev/null +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -0,0 +1,94 @@ + +from __future__ import annotations + +import asyncio +import time + +from typing import Optional, List, Callable + +from bluesky.protocols import Locatable, Stoppable, Location + +from tango import DevState + +from ophyd_async.tango import TangoReadableDevice, tango_signal_x, tango_signal_r, tango_signal_rw +from ophyd_async.core import AsyncStatus + + +# -------------------------------------------------------------------- +class OmsVME58Motor(TangoReadableDevice, Locatable, Stoppable): + + # -------------------------------------------------------------------- + def __init__(self, trl: str, name="") -> None: + TangoReadableDevice.__init__(self, trl, name) + self._set_success = True + + # -------------------------------------------------------------------- + def register_signals(self): + + self.position = tango_signal_rw(float, self.trl + '/position', device_proxy=self.proxy) + self.baserate = tango_signal_rw(int, self.trl + '/baserate', device_proxy=self.proxy) + self.slewrate = tango_signal_rw(int, self.trl + '/slewrate', device_proxy=self.proxy) + self.conversion = tango_signal_rw(float, self.trl + '/conversion', device_proxy=self.proxy) + self.acceleration = tango_signal_rw(int, self.trl + '/acceleration', device_proxy=self.proxy) + + self.set_readable_signals(read_uncached=[self.position], + config=[self.baserate, + self.slewrate, + self.conversion, + self.acceleration]) + + self._stop = tango_signal_x(self.trl + '/stopmove', self.proxy) + self._state = tango_signal_r(DevState, self.trl + '/state', self.proxy) + + # -------------------------------------------------------------------- + async def _move(self, new_position: float, watchers: List[Callable] = []): + self._set_success = True + start = time.monotonic() + start_position = await self.position.get_value() + + def update_watchers(current_position: float): + for watcher in watchers: + watcher( + name=self.name, + current=current_position, + initial=start_position, + target=new_position, + time_elapsed=time.monotonic() - start, + ) + + if self.position.is_cachable(): + self.position.subscribe_value(update_watchers) + else: + update_watchers(start_position) + try: + await self.position.set(new_position) + await asyncio.sleep(0.1) + while await self._state.get_value() == DevState.MOVING: + await asyncio.sleep(0.1) + finally: + if self.position.is_cachable(): + self.position.clear_sub(update_watchers) + else: + update_watchers(await self.position.get_value()) + if not self._set_success: + raise RuntimeError("Motor was stopped") + + # -------------------------------------------------------------------- + def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus: + watchers: List[Callable] = [] + coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout) + return AsyncStatus(coro, watchers) + + # -------------------------------------------------------------------- + async def locate(self) -> Location: + set_point = await self.position.get_setpoint() + readback = await self.position.get_value() + return Location(setpoint=set_point, readback=readback) + + # -------------------------------------------------------------------- + async def stop(self, success=False): + self._set_success = success + # Put with completion will never complete as we are waiting for completion on + # the move above, so need to pass wait=False + await self._stop.execute(wait=False) + diff --git a/src/ophyd_async/tango/device_controllers/sis3820.py b/src/ophyd_async/tango/device_controllers/sis3820.py new file mode 100644 index 0000000000..fb7a140076 --- /dev/null +++ b/src/ophyd_async/tango/device_controllers/sis3820.py @@ -0,0 +1,40 @@ + +from __future__ import annotations + +from typing import Dict + +# from bluesky.protocols import Triggerable +from bluesky.protocols import Reading + +from ophyd_async.tango import TangoReadableDevice, tango_signal_rw, tango_signal_x + + +# -------------------------------------------------------------------- +class SIS3820Counter(TangoReadableDevice): # Triggerable + + # -------------------------------------------------------------------- + def __init__(self, trl: str, name="") -> None: + TangoReadableDevice.__init__(self, trl, name) + self._set_success = True + + # -------------------------------------------------------------------- + def register_signals(self): + + self.counts = tango_signal_rw(float, self.trl + '/counts', device_proxy=self.proxy) + self.offset = tango_signal_rw(float, self.trl + '/offset', device_proxy=self.proxy) + + self.set_readable_signals(read_uncached=[self.counts], + config=[self.offset]) + + self.reset = tango_signal_x(self.trl + '/reset', device_proxy=self.proxy) + + # -------------------------------------------------------------------- + # Theoretically counter has to be reset before triggering, but I do not how to do it + # def trigger(self) -> AsyncStatus: + # return self.reset.trigger() + + # -------------------------------------------------------------------- + async def read(self) -> Dict[str, Reading]: + ret = await super().read() + await self.reset.trigger() + return ret diff --git a/src/ophyd_async/tango/sardana/__init__.py b/src/ophyd_async/tango/sardana/__init__.py index 3c77828dc9..2ba08cb7b5 100644 --- a/src/ophyd_async/tango/sardana/__init__.py +++ b/src/ophyd_async/tango/sardana/__init__.py @@ -1,3 +1,3 @@ -from ophyd_async.tango.sardana.devices import SardanaMotor +from ophyd_async.tango.sardana.motor import SardanaMotor __all__ = ("SardanaMotor",) \ No newline at end of file diff --git a/src/ophyd_async/tango/sardana/devices.py b/src/ophyd_async/tango/sardana/motor.py similarity index 98% rename from src/ophyd_async/tango/sardana/devices.py rename to src/ophyd_async/tango/sardana/motor.py index af90b770a7..c79ddd9a3d 100644 --- a/src/ophyd_async/tango/sardana/devices.py +++ b/src/ophyd_async/tango/sardana/motor.py @@ -56,6 +56,7 @@ def update_watchers(current_position: float): time_elapsed=time.monotonic() - start, ) + # raise RuntimeError("Test") self.position.subscribe_value(update_watchers) try: await self.position.set(new_position) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 49abde98d1..8d76d58f84 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -8,9 +8,10 @@ from tango import AttrWriteType, CmdArgType -from ophyd_async.core import SignalR, SignalX, T +from ophyd_async.core import T -from ophyd_async.tango._backend import TangoTransport, TangoSignalRW, TangoSignalW, TangoSignalBackend +from ophyd_async.tango._backend import (TangoTransport, TangoSignalRW, TangoSignalW, + TangoSignalX, TangoSignalR, TangoSignalBackend) __all__ = ("tango_signal_rw", "tango_signal_r", @@ -46,7 +47,7 @@ def tango_signal_rw(datatype: Type[T], def tango_signal_r(datatype: Type[T], read_trl: str, device_proxy: Optional[DeviceProxy] = None - ) -> SignalR[T]: + ) -> TangoSignalR[T]: """Create a `SignalR` backed by 1 Tango Attribute/Command Parameters @@ -59,7 +60,7 @@ def tango_signal_r(datatype: Type[T], If given, this DeviceProxy will be used """ backend = TangoTransport(datatype, read_trl, read_trl, device_proxy) - return SignalR(backend) + return TangoSignalR(backend) # -------------------------------------------------------------------- @@ -85,7 +86,7 @@ def tango_signal_w(datatype: Type[T], # -------------------------------------------------------------------- def tango_signal_x(write_trl: str, device_proxy: Optional[DeviceProxy] = None - ) -> SignalX: + ) -> TangoSignalX: """Create a `SignalX` backed by 1 Tango Attribute/Command Parameters @@ -96,12 +97,12 @@ def tango_signal_x(write_trl: str, If given, this DeviceProxy will be used """ backend: TangoSignalBackend = TangoTransport(None, write_trl, write_trl, device_proxy) - return SignalX(backend) + return TangoSignalX(backend) # -------------------------------------------------------------------- def tango_signal_auto(datatype: Type[T], full_trl: str, device_proxy: Optional[DeviceProxy] = None) -> \ - Union[TangoSignalW, SignalX, SignalR, TangoSignalRW]: + Union[TangoSignalW, TangoSignalX, TangoSignalR, TangoSignalRW]: backend: TangoSignalBackend = TangoTransport(datatype, full_trl, full_trl, device_proxy) device_trl, tr_name = full_trl.rsplit('/', 1) @@ -111,18 +112,18 @@ def tango_signal_auto(datatype: Type[T], full_trl: str, device_proxy: Optional[D if config.writable in [AttrWriteType.READ_WRITE, AttrWriteType.READ_WITH_WRITE]: return TangoSignalRW(backend) elif config.writable == AttrWriteType.READ: - return SignalR(backend) + return TangoSignalR(backend) else: return TangoSignalW(backend) if tr_name in device_proxy.get_command_list(): config = device_proxy.get_command_config(tr_name) if config.in_type == CmdArgType.DevVoid: - return SignalX(backend) + return TangoSignalX(backend) elif config.out_type != CmdArgType.DevVoid: return TangoSignalRW(backend) else: - return SignalR(backend) + return TangoSignalX(backend) if tr_name in device_proxy.get_pipe_list(): raise NotImplemented("Pipes are not supported") diff --git a/tests/tango/test_device.py b/tests/tango/test_base_device.py similarity index 100% rename from tests/tango/test_device.py rename to tests/tango/test_base_device.py diff --git a/tests/tango/test_ddg2.py b/tests/tango/test_ddg2.py new file mode 100644 index 0000000000..cdce9cbf8c --- /dev/null +++ b/tests/tango/test_ddg2.py @@ -0,0 +1,55 @@ +import pytest + +from unittest.mock import Mock + +from tango.asyncio_executor import set_global_executor + +from ophyd_async.tango.device_controllers import DGG2Timer +from ophyd_async.core import DeviceCollector + +from bluesky import RunEngine +from bluesky.plans import count + +# -------------------------------------------------------------------- +@pytest.fixture(autouse=True) +def reset_tango_asyncio(): + set_global_executor(None) + + +# -------------------------------------------------------------------- +@pytest.fixture +async def dgg2(): + async with DeviceCollector(): + dgg2 = await DGG2Timer("p09/dgg2/eh.01") + + yield dgg2 + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_connect(dgg2): + + assert dgg2.name == "dgg2" + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_readout_with_bluesky(dgg2): + + TEST_TIME = 0.1 + + await dgg2.set_time(TEST_TIME) + readouts = Mock() + + RE = RunEngine() + RE(count([dgg2], 3), readouts) + + description = [args[0][1]['configuration'] for args in readouts.call_args_list if args[0][0] == "descriptor"][0] + assert "dgg2" in description + assert 'dgg2-sampletime' in description["dgg2"]["data"] + assert description["dgg2"]["data"]["dgg2-sampletime"] == pytest.approx(TEST_TIME, abs=0.1) + + readings = [args[0][1]['data'] for args in readouts.call_args_list if args[0][0] == "event"] + assert len(readings) == 3 + assert "dgg2-sampletime" in readings[0] + assert readings[0]["dgg2-sampletime"] == pytest.approx(TEST_TIME, abs=0.1) \ No newline at end of file diff --git a/tests/tango/test_sardana.py b/tests/tango/test_motors.py similarity index 85% rename from tests/tango/test_sardana.py rename to tests/tango/test_motors.py index ccd15c5cd0..d1d1d972c9 100644 --- a/tests/tango/test_sardana.py +++ b/tests/tango/test_motors.py @@ -8,6 +8,7 @@ from tango.asyncio_executor import set_global_executor from ophyd_async.tango.sardana import SardanaMotor +from ophyd_async.tango.device_controllers import OmsVME58Motor from ophyd_async.core import DeviceCollector from bluesky import RunEngine @@ -19,6 +20,22 @@ # all the tasks have a chance to run A_BIT = 0.001 +# dict: {class: tango trl} +MOTORS_TO_TEST = { + SardanaMotor: "motor/dummy_mot_ctrl/1", + OmsVME58Motor: "p09/motor/eh.01" +} + + +# -------------------------------------------------------------------- +@pytest.fixture( + params=list(MOTORS_TO_TEST.items()), + ids=list(MOTORS_TO_TEST.keys()), +) +def motor_to_test(request): + return request.param + + # -------------------------------------------------------------------- @pytest.fixture(autouse=True) def reset_tango_asyncio(): @@ -27,9 +44,9 @@ def reset_tango_asyncio(): # -------------------------------------------------------------------- @pytest.fixture -async def dummy_motor(): +async def dummy_motor(motor_to_test): async with DeviceCollector(): - dummy_motor = await SardanaMotor("motor/dummy_mot_ctrl/1") + dummy_motor = await motor_to_test[0](motor_to_test[1]) yield dummy_motor diff --git a/tests/tango/test_scans.py b/tests/tango/test_scans.py new file mode 100644 index 0000000000..7161e099b7 --- /dev/null +++ b/tests/tango/test_scans.py @@ -0,0 +1,55 @@ +import pytest +import asyncio + +import numpy as np + +from unittest.mock import Mock, call + +from tango.asyncio_executor import set_global_executor + +from ophyd_async.tango.device_controllers import OmsVME58Motor, DGG2Timer, SIS3820Counter +from ophyd_async.core import DeviceCollector + +from bluesky import RunEngine +from bluesky.plans import scan + + +# Long enough for multiple asyncio event loop cycles to run so +# all the tasks have a chance to run +A_BIT = 0.001 + +# -------------------------------------------------------------------- +@pytest.fixture(autouse=True) +def reset_tango_asyncio(): + set_global_executor(None) + + +# -------------------------------------------------------------------- +@pytest.fixture +async def devices_set(): + async with DeviceCollector(): + omsvme58_motor = await OmsVME58Motor("p09/motor/eh.01") + dgg2timer = await DGG2Timer("p09/dgg2/eh.01") + sis3820 = await SIS3820Counter("p09/counter/eh.01") + + yield [omsvme58_motor, dgg2timer, sis3820] + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_step_scan_motor_vs_counter_with_dgg2(devices_set): + + omsvme58_motor, dgg2timer, sis3820 = devices_set + + readouts = Mock() + + # now let's do some bluesky stuff + RE = RunEngine() + RE(scan([omsvme58_motor, sis3820, dgg2timer], omsvme58_motor, 0, 1, num=11), readouts) + + assert readouts.call_count == 14 + assert set([args[0][0] for args in readouts.call_args_list]) == {'descriptor', 'event', 'start', 'stop'} + + positions = [args[0][1]['data']['omsvme58_motor-position'] for args in readouts.call_args_list if args[0][0] == "event"] + for got, expected in zip(positions, np.arange(0, 1.1, 0.1)): + assert pytest.approx(got, abs=0.1) == expected \ No newline at end of file diff --git a/tests/tango/test_sis3820.py b/tests/tango/test_sis3820.py new file mode 100644 index 0000000000..057a4532b3 --- /dev/null +++ b/tests/tango/test_sis3820.py @@ -0,0 +1,52 @@ +import pytest + +from unittest.mock import Mock + +from tango.asyncio_executor import set_global_executor + +from ophyd_async.tango.device_controllers import SIS3820Counter +from ophyd_async.core import DeviceCollector + +from bluesky import RunEngine +from bluesky.plans import count + + +# -------------------------------------------------------------------- +@pytest.fixture(autouse=True) +def reset_tango_asyncio(): + set_global_executor(None) + + +# -------------------------------------------------------------------- +@pytest.fixture +async def sis3820(): + async with DeviceCollector(): + sis3820 = await SIS3820Counter("p09/counter/eh.01") + + yield sis3820 + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_connect(sis3820): + + assert sis3820.name == "sis3820" + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_readout_with_bluesky(sis3820): + + readouts = Mock() + # now let's do some bluesky stuff + + RE = RunEngine() + RE(count([sis3820], 3), readouts) + + description = [args[0][1]['configuration'] for args in readouts.call_args_list if args[0][0] == "descriptor"][0] + assert "sis3820" in description + assert 'sis3820-offset' in description["sis3820"]["data"] + + readings = [args[0][1]['data'] for args in readouts.call_args_list if args[0][0] == "event"] + assert len(readings) == 3 + assert "sis3820-counts" in readings[0] \ No newline at end of file From 5506c6b842324c6b76d74fad391fcd5da66fc2ef Mon Sep 17 00:00:00 2001 From: matveyev Date: Mon, 15 Jan 2024 18:23:22 +0100 Subject: [PATCH 010/141] added test to examples --- docs/user/examples/tango_scan.py | 32 +++++++++++++++++++ .../tango/device_controllers/omsvme58.py | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 docs/user/examples/tango_scan.py diff --git a/docs/user/examples/tango_scan.py b/docs/user/examples/tango_scan.py new file mode 100644 index 0000000000..a104d54e84 --- /dev/null +++ b/docs/user/examples/tango_scan.py @@ -0,0 +1,32 @@ +# simple example to scan motor and acquire in each point counter value +import asyncio + +from ophyd_async.core.utils import merge_gathered_dicts +from ophyd_async.tango.device_controllers import OmsVME58Motor, DGG2Timer, SIS3820Counter +from ophyd_async.core import DeviceCollector + +from bluesky import RunEngine +from bluesky.callbacks import LiveTable +from bluesky.plans import scan + + +# -------------------------------------------------------------------- +async def main(): + # first, connect all necessary devices. Note, that Tango devices have to be awaited! + async with DeviceCollector(): + omsvme58_motor = await OmsVME58Motor("p09/motor/eh.01") + dgg2timer = await DGG2Timer("p09/dgg2/eh.01") + sis3820 = await SIS3820Counter("p09/counter/eh.01") + + # create engine + RE = RunEngine() + # do scan with LiveTable output + # (seems LiveTable cannot work with async devices, so we have to generate keys by ourselves...) + dets = [omsvme58_motor, sis3820, dgg2timer] + dets_descr = await merge_gathered_dicts([det.describe() for det in dets]) + RE(scan(dets, omsvme58_motor, 0, 1, num=11), LiveTable(list(dets_descr.keys()))) + + +# -------------------------------------------------------------------- +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index e41a7403f5..790a60bea7 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -90,5 +90,5 @@ async def stop(self, success=False): self._set_success = success # Put with completion will never complete as we are waiting for completion on # the move above, so need to pass wait=False - await self._stop.execute(wait=False) + await self._stop.trigger() From 1529376f722f570c6aef1a652ad1e5a1b5a3f6ba Mon Sep 17 00:00:00 2001 From: matveyev Date: Tue, 16 Jan 2024 10:41:00 +0100 Subject: [PATCH 011/141] dgg2 -> Preparable, added set time to example --- docs/user/examples/tango_scan.py | 11 ++++++++++- src/ophyd_async/core/flyer.py | 8 ++++---- src/ophyd_async/tango/device_controllers/dgg2.py | 8 ++++++-- tests/tango/test_ddg2.py | 5 ++++- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/user/examples/tango_scan.py b/docs/user/examples/tango_scan.py index a104d54e84..75437b4f24 100644 --- a/docs/user/examples/tango_scan.py +++ b/docs/user/examples/tango_scan.py @@ -5,10 +5,12 @@ from ophyd_async.tango.device_controllers import OmsVME58Motor, DGG2Timer, SIS3820Counter from ophyd_async.core import DeviceCollector -from bluesky import RunEngine +from bluesky import RunEngine, Msg from bluesky.callbacks import LiveTable from bluesky.plans import scan +ACQUISITION_TIME = 0.1 + # -------------------------------------------------------------------- async def main(): @@ -18,8 +20,15 @@ async def main(): dgg2timer = await DGG2Timer("p09/dgg2/eh.01") sis3820 = await SIS3820Counter("p09/counter/eh.01") + # to set acquisition time we can use set_time method of dgg2timer + await dgg2timer.set_time(ACQUISITION_TIME) + # create engine RE = RunEngine() + + # more "Blueskyisch" set of acquisition time + RE([Msg("prepare", dgg2timer, ACQUISITION_TIME)]) + # do scan with LiveTable output # (seems LiveTable cannot work with async devices, so we have to generate keys by ourselves...) dets = [omsvme58_motor, sis3820, dgg2timer] diff --git a/src/ophyd_async/core/flyer.py b/src/ophyd_async/core/flyer.py index 0d46c7a006..b6c83df4f6 100644 --- a/src/ophyd_async/core/flyer.py +++ b/src/ophyd_async/core/flyer.py @@ -15,12 +15,12 @@ from bluesky.protocols import ( Asset, - # Collectable, + Collectable, Descriptor, Flyable, HasHints, Hints, - # Preparable, + Preparable, Reading, Stageable, WritesExternalAssets, @@ -174,10 +174,10 @@ async def stop(self): class HardwareTriggeredFlyable( Device, - # Preparable, + Preparable, Stageable, Flyable, - # Collectable, + Collectable, WritesExternalAssets, HasHints, Generic[T], diff --git a/src/ophyd_async/tango/device_controllers/dgg2.py b/src/ophyd_async/tango/device_controllers/dgg2.py index a70ce83fd6..4164882566 100644 --- a/src/ophyd_async/tango/device_controllers/dgg2.py +++ b/src/ophyd_async/tango/device_controllers/dgg2.py @@ -5,14 +5,14 @@ import time from typing import List, Callable -from bluesky.protocols import Triggerable +from bluesky.protocols import Triggerable, Preparable from ophyd_async.core import AsyncStatus from ophyd_async.tango import TangoReadableDevice, tango_signal_rw, tango_signal_x # -------------------------------------------------------------------- -class DGG2Timer(TangoReadableDevice, Triggerable): +class DGG2Timer(TangoReadableDevice, Triggerable, Preparable): # -------------------------------------------------------------------- def __init__(self, trl: str, name="") -> None: @@ -65,6 +65,10 @@ def trigger(self) -> AsyncStatus: watchers: List[Callable] = [] return AsyncStatus(self._trigger(watchers), watchers) + # -------------------------------------------------------------------- + def prepare(self, time) -> AsyncStatus: + return self.sampletime.set(time) + # -------------------------------------------------------------------- async def set_time(self, time: float) -> None: await self.sampletime.set(time) diff --git a/tests/tango/test_ddg2.py b/tests/tango/test_ddg2.py index cdce9cbf8c..d8ae3fe482 100644 --- a/tests/tango/test_ddg2.py +++ b/tests/tango/test_ddg2.py @@ -7,9 +7,10 @@ from ophyd_async.tango.device_controllers import DGG2Timer from ophyd_async.core import DeviceCollector -from bluesky import RunEngine +from bluesky import RunEngine, Msg from bluesky.plans import count + # -------------------------------------------------------------------- @pytest.fixture(autouse=True) def reset_tango_asyncio(): @@ -42,6 +43,8 @@ async def test_readout_with_bluesky(dgg2): readouts = Mock() RE = RunEngine() + + RE([Msg("prepare", dgg2, TEST_TIME)]) RE(count([dgg2], 3), readouts) description = [args[0][1]['configuration'] for args in readouts.call_args_list if args[0][0] == "descriptor"][0] From 1caf5abec524888e75488a0fd55ae45d676bcc1a Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 26 Feb 2024 09:18:59 +0100 Subject: [PATCH 012/141] Renamed test to avoid duplicate names --- tests/tango/{test_signals.py => test_tango_signals.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/tango/{test_signals.py => test_tango_signals.py} (100%) diff --git a/tests/tango/test_signals.py b/tests/tango/test_tango_signals.py similarity index 100% rename from tests/tango/test_signals.py rename to tests/tango/test_tango_signals.py From fc94a5d43bd1d3573502b8c62e81a283b12839b3 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 26 Feb 2024 09:37:54 +0100 Subject: [PATCH 013/141] Added timeout to TangoTransport.connect() signature. All pytests pass --- src/ophyd_async/tango/_backend/_tango_transport.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 33680d884b..3edd0a1ddd 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -28,6 +28,7 @@ get_dtype, get_unique, wait_for_connection, + DEFAULT_TIMEOUT, ) __all__ = ("TangoTransport", "TangoSignalBackend") @@ -389,7 +390,7 @@ async def _connect_and_store_config(self, trl): raise NotConnected(self.source) # -------------------------------------------------------------------- - async def connect(self): + async def connect(self, timeout: float = DEFAULT_TIMEOUT): if self.read_trl != self.write_trl: # Different, need to connect both await wait_for_connection( From fc0c0f5090ec7cb976a838328e6c3c3a9f63abc4 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 27 Feb 2024 10:33:18 +0100 Subject: [PATCH 014/141] Added pytango to dependencies --- .devcontainer/Dockerfile | 37 -------------------------- .devcontainer/devcontainer.json | 47 --------------------------------- pyproject.toml | 1 + 3 files changed, 1 insertion(+), 84 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 349a45cc6f..0000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -# This file is for use as a devcontainer and a runtime container -# -# The devcontainer should use the build target and run as root with podman -# or docker with user namespaces. -# -FROM python:3.9 as build - -ARG PIP_OPTIONS - -# Add any system dependencies for the developer/build environment here e.g. -# RUN apt-get update && apt-get upgrade -y && \ -# apt-get install -y --no-install-recommends \ -# desired-packages \ -# && rm -rf /var/lib/apt/lists/* - -# set up a virtual environment and put it in PATH -RUN python -m venv /venv -ENV PATH=/venv/bin:$PATH - -# Copy any required context for the pip install over -COPY . /context -WORKDIR /context - -# install python package into /venv -RUN pip install ${PIP_OPTIONS} - -FROM python:3.9-slim as runtime - -# Add apt-get system dependecies for runtime here if needed - -# copy the virtual environment from the build stage and put it in PATH -COPY --from=build /venv/ /venv/ -ENV PATH=/venv/bin:$PATH - -# change this entrypoint if it is not the same as the repo -ENTRYPOINT ["python", "-m", "ophyd_epics_devices"] -CMD ["--version"] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 314a151af7..0000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,47 +0,0 @@ -// For format details, see https://containers.dev/implementors/json_reference/ -{ - "name": "Python 3 Developer Container", - "build": { - "dockerfile": "Dockerfile", - "target": "build", - // Only upgrade pip, we will install the project below - "args": { - "PIP_OPTIONS": "--upgrade pip" - }, - }, - "remoteEnv": { - "DISPLAY": "${localEnv:DISPLAY}" - }, - // Set *default* container specific settings.json values on container create. - "settings": { - "python.defaultInterpreterPath": "/venv/bin/python" - }, - "customizations": { - "vscode": { - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "ms-python.python", - "tamasfe.even-better-toml", - "redhat.vscode-yaml", - "ryanluker.vscode-coverage-gutters" - ] - } - }, - // Make sure the files we are mapping into the container exist on the host - "initializeCommand": "bash -c 'for i in $HOME/.inputrc; do [ -f $i ] || touch $i; done'", - "runArgs": [ - "--net=host", - "--security-opt=label=type:container_runtime_t" - ], - "mounts": [ - "source=${localEnv:HOME}/.ssh,target=/root/.ssh,type=bind", - "source=${localEnv:HOME}/.inputrc,target=/root/.inputrc,type=bind", - // map in home directory - not strictly necessary but useful - "source=${localEnv:HOME},target=${localEnv:HOME},type=bind,consistency=cached" - ], - // make the workspace folder the same inside and outside of the container - "workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind", - "workspaceFolder": "${localWorkspaceFolder}", - // After the container is created, install the python project in editable form - "postCreateCommand": "pip install -e .[dev] --config-settings editable_mode=compat" -} diff --git a/pyproject.toml b/pyproject.toml index 3aecff7240..94aa0a6cfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dev = [ "pydata-sphinx-theme>=0.12", "pyepics>=3.4.2", "pyside6", + "pytango", "pytest", "pytest-asyncio", "pytest-cov", From 64db40a007999e9eecdfda61de6855ae55f9b807 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 27 Feb 2024 14:05:27 +0100 Subject: [PATCH 015/141] Bringing code to linting compliance with black, flake8 and isort. --- docs/user/examples/tango_scan.py | 19 +- src/ophyd_async/core/utils.py | 1 - src/ophyd_async/tango/__init__.py | 10 +- src/ophyd_async/tango/_backend/__init__.py | 26 +- .../tango/_backend/_signal_backend.py | 14 +- .../tango/_backend/_tango_transport.py | 180 +++++++++----- .../tango/base_devices/base_device.py | 4 +- .../tango/device_controllers/__init__.py | 6 +- .../tango/device_controllers/dgg2.py | 39 +-- .../tango/device_controllers/omsvme58.py | 52 ++-- .../tango/device_controllers/sis3820.py | 14 +- src/ophyd_async/tango/sardana/__init__.py | 2 +- src/ophyd_async/tango/sardana/motor.py | 58 +++-- src/ophyd_async/tango/signal/__init__.py | 10 +- src/ophyd_async/tango/signal/signal.py | 81 ++++--- tests/tango/test_base_device.py | 105 +++++--- tests/tango/test_ddg2.py | 32 ++- tests/tango/test_motors.py | 42 ++-- tests/tango/test_scans.py | 42 ++-- tests/tango/test_sis3820.py | 24 +- tests/tango/test_tango_signals.py | 229 +++++++++++------- 21 files changed, 621 insertions(+), 369 deletions(-) diff --git a/docs/user/examples/tango_scan.py b/docs/user/examples/tango_scan.py index 75437b4f24..92bc896a00 100644 --- a/docs/user/examples/tango_scan.py +++ b/docs/user/examples/tango_scan.py @@ -1,14 +1,18 @@ # simple example to scan motor and acquire in each point counter value import asyncio -from ophyd_async.core.utils import merge_gathered_dicts -from ophyd_async.tango.device_controllers import OmsVME58Motor, DGG2Timer, SIS3820Counter -from ophyd_async.core import DeviceCollector - -from bluesky import RunEngine, Msg +from bluesky import Msg, RunEngine from bluesky.callbacks import LiveTable from bluesky.plans import scan +from ophyd_async.core import DeviceCollector +from ophyd_async.core.utils import merge_gathered_dicts +from ophyd_async.tango.device_controllers import ( + DGG2Timer, + OmsVME58Motor, + SIS3820Counter, +) + ACQUISITION_TIME = 0.1 @@ -30,7 +34,8 @@ async def main(): RE([Msg("prepare", dgg2timer, ACQUISITION_TIME)]) # do scan with LiveTable output - # (seems LiveTable cannot work with async devices, so we have to generate keys by ourselves...) + # (seems LiveTable cannot work with async devices, + # so we have to generate keys by ourselves...) dets = [omsvme58_motor, sis3820, dgg2timer] dets_descr = await merge_gathered_dicts([det.describe() for det in dets]) RE(scan(dets, omsvme58_motor, 0, 1, num=11), LiveTable(list(dets_descr.keys()))) @@ -38,4 +43,4 @@ async def main(): # -------------------------------------------------------------------- if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) diff --git a/src/ophyd_async/core/utils.py b/src/ophyd_async/core/utils.py index ac461f1820..42a4b5b7d4 100644 --- a/src/ophyd_async/core/utils.py +++ b/src/ophyd_async/core/utils.py @@ -14,7 +14,6 @@ Union, ) -from tango import EventData import numpy as np from bluesky.protocols import Reading diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py index f52d66a165..88276fc73c 100644 --- a/src/ophyd_async/tango/__init__.py +++ b/src/ophyd_async/tango/__init__.py @@ -1,5 +1,11 @@ -from ophyd_async.tango.signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x, tango_signal_auto from ophyd_async.tango.base_devices import TangoReadableDevice +from ophyd_async.tango.signal import ( + tango_signal_auto, + tango_signal_r, + tango_signal_rw, + tango_signal_w, + tango_signal_x, +) __all__ = [ "tango_signal_r", @@ -7,5 +13,5 @@ "tango_signal_w", "tango_signal_x", "tango_signal_auto", - "TangoReadableDevice" + "TangoReadableDevice", ] diff --git a/src/ophyd_async/tango/_backend/__init__.py b/src/ophyd_async/tango/_backend/__init__.py index 5f55d915be..1cd76bef80 100644 --- a/src/ophyd_async/tango/_backend/__init__.py +++ b/src/ophyd_async/tango/_backend/__init__.py @@ -1,9 +1,19 @@ -from ophyd_async.tango._backend._tango_transport import TangoTransport, TangoSignalBackend -from ophyd_async.tango._backend._signal_backend import TangoSignalW, TangoSignalRW, TangoSignalR, TangoSignalX +from ophyd_async.tango._backend._signal_backend import ( + TangoSignalR, + TangoSignalRW, + TangoSignalW, + TangoSignalX, +) +from ophyd_async.tango._backend._tango_transport import ( + TangoSignalBackend, + TangoTransport, +) -__all__ = ("TangoTransport", - "TangoSignalBackend", - "TangoSignalW", - "TangoSignalRW", - "TangoSignalR", - "TangoSignalX") \ No newline at end of file +__all__ = ( + "TangoTransport", + "TangoSignalBackend", + "TangoSignalW", + "TangoSignalRW", + "TangoSignalR", + "TangoSignalX", +) diff --git a/src/ophyd_async/tango/_backend/_signal_backend.py b/src/ophyd_async/tango/_backend/_signal_backend.py index bc17fa1fb6..e93714dc11 100644 --- a/src/ophyd_async/tango/_backend/_signal_backend.py +++ b/src/ophyd_async/tango/_backend/_signal_backend.py @@ -2,7 +2,7 @@ from typing import Optional -from ophyd_async.core import T, SignalW, SignalRW, SignalR, SignalX +from ophyd_async.core import SignalR, SignalRW, SignalW, SignalX, T from ophyd_async.core.signal import add_timeout @@ -24,20 +24,16 @@ def is_cachable(self) -> T: # -------------------------------------------------------------------- -class TangoSignalW(SignalW[T], CachableOrNot, SignalWithSetpoit): - ... +class TangoSignalW(SignalW[T], CachableOrNot, SignalWithSetpoit): ... # noqa: E701 # -------------------------------------------------------------------- -class TangoSignalRW(SignalRW[T], CachableOrNot, SignalWithSetpoit): - ... +class TangoSignalRW(SignalRW[T], CachableOrNot, SignalWithSetpoit): ... # noqa: E701 # -------------------------------------------------------------------- -class TangoSignalR(SignalR[T], CachableOrNot): - ... +class TangoSignalR(SignalR[T], CachableOrNot): ... # noqa: E701 # -------------------------------------------------------------------- -class TangoSignalX(SignalX, CachableOrNot): - ... +class TangoSignalX(SignalX, CachableOrNot): ... # noqa: E701 diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 3edd0a1ddd..8c15a37e2d 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -1,26 +1,31 @@ import asyncio -import time import functools - -import numpy as np -from asyncio import CancelledError - +import time from abc import abstractmethod +from asyncio import CancelledError from enum import Enum from typing import Dict, Optional, Type, Union -from tango import (AttributeInfoEx, AttrDataFormat, - CmdArgType, EventType, - GreenMode, DevState, - CommandInfo, AttrWriteType) - -from tango.asyncio import DeviceProxy -from tango.asyncio_executor import get_global_executor, set_global_executor, AsyncioExecutor -from tango.utils import is_int, is_float, is_bool, is_str, is_binary, is_array - +import numpy as np from bluesky.protocols import Descriptor, Reading +from tango import ( + AttrDataFormat, + AttributeInfoEx, + CmdArgType, + CommandInfo, + DevState, + EventType, +) +from tango.asyncio import DeviceProxy +from tango.asyncio_executor import ( + AsyncioExecutor, + get_global_executor, + set_global_executor, +) +from tango.utils import is_array, is_binary, is_bool, is_float, is_int, is_str from ophyd_async.core import ( + DEFAULT_TIMEOUT, NotConnected, ReadingValueCallback, SignalBackend, @@ -28,7 +33,6 @@ get_dtype, get_unique, wait_for_connection, - DEFAULT_TIMEOUT, ) __all__ = ("TangoTransport", "TangoSignalBackend") @@ -39,6 +43,7 @@ # -------------------------------------------------------------------- + def ensure_proper_executor(func): @functools.wraps(func) async def wrapper(self, *args, **kwargs): @@ -98,7 +103,8 @@ def __init__(self, device_proxy: DeviceProxy, name: str): # -------------------------------------------------------------------- async def connect(self): - """perform actions after proxy is connected, e.g. checks if signal can be subscribed""" + """perform actions after proxy is connected, e.g. checks if signal + can be subscribed""" # -------------------------------------------------------------------- @abstractmethod @@ -112,7 +118,9 @@ async def get_w_value(self) -> T: # -------------------------------------------------------------------- @abstractmethod - async def put(self, value: Optional[T], wait: bool=True, timeout: Optional[float]=None) -> None: + async def put( + self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None + ) -> None: """Put value to TRL""" # -------------------------------------------------------------------- @@ -150,7 +158,9 @@ class AttributeProxy(TangoProxy): # -------------------------------------------------------------------- async def connect(self) -> None: try: - eid = await self._proxy.subscribe_event(self._name, EventType.CHANGE_EVENT, self._event_processor) + eid = await self._proxy.subscribe_event( + self._name, EventType.CHANGE_EVENT, self._event_processor + ) await self._proxy.unsubscribe_event(eid) self.support_events = True except Exception: @@ -170,7 +180,9 @@ async def get_w_value(self) -> T: # -------------------------------------------------------------------- @ensure_proper_executor - async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None) -> None: + async def put( + self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None + ) -> None: if wait: await self._proxy.write_attribute(self._name, value) else: @@ -181,7 +193,7 @@ async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[flo try: _ = await self._proxy.write_attribute_reply(rid) finished = True - except: + except Exception: await asyncio.sleep(A_BIT) # -------------------------------------------------------------------- @@ -208,8 +220,12 @@ def subscribe_callback(self, callback: Optional[ReadingValueCallback]): """add user callback to CHANGE event subscription""" self._event_callback = callback if not self._eid: - self._eid = self._proxy.subscribe_event(self._name, EventType.CHANGE_EVENT, self._event_processor, - green_mode=False) + self._eid = self._proxy.subscribe_event( + self._name, + EventType.CHANGE_EVENT, + self._event_processor, + green_mode=False, + ) # -------------------------------------------------------------------- def unsubscribe_callback(self): @@ -222,9 +238,11 @@ def unsubscribe_callback(self): def _event_processor(self, event): if not event.err: value = event.attr_value.value - reading = dict(value=value, - timestamp=event.get_date().totime(), - alarm_severity=event.attr_value.quality) + reading = dict( + value=value, + timestamp=event.get_date().totime(), + alarm_severity=event.attr_value.quality, + ) self._event_callback(reading, value) @@ -241,7 +259,9 @@ async def get(self) -> T: # -------------------------------------------------------------------- @ensure_proper_executor - async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None) -> None: + async def put( + self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None + ) -> None: if wait: val = await self._proxy.command_inout(self._name, value) else: @@ -253,7 +273,7 @@ async def put(self, value: Optional[T], wait: bool = True, timeout: Optional[flo try: val = await self._proxy.command_inout_reply(rid) finished = True - except: + except Exception: await asyncio.sleep(A_BIT) self._last_reading = dict(value=val, timestamp=time.time(), alarm_severity=0) @@ -273,33 +293,51 @@ def get_dtype_extended(datatype): # DevState tango type does not have numpy equivalents dtype = get_dtype(datatype) if dtype == np.object_: - print(f"{datatype.__args__[1].__args__[0]=}, {datatype.__args__[1].__args__[0]==Enum}") + print( + f"{datatype.__args__[1].__args__[0]=}," + f"{datatype.__args__[1].__args__[0]==Enum}" + ) if datatype.__args__[1].__args__[0] == DevState: dtype = CmdArgType.DevState return dtype # -------------------------------------------------------------------- -def get_trl_descriptor(datatype: Optional[Type], tango_resource: str, - tr_configs: Dict[str, Union[AttributeInfoEx, CommandInfo]]) -> dict: +def get_trl_descriptor( + datatype: Optional[Type], + tango_resource: str, + tr_configs: Dict[str, Union[AttributeInfoEx, CommandInfo]], +) -> dict: tr_dtype = {} for tr_name, config in tr_configs.items(): if isinstance(config, AttributeInfoEx): _, dtype, descr = get_pyton_type(config.data_type) tr_dtype[tr_name] = config.data_format, dtype, descr elif isinstance(config, CommandInfo): - if config.in_type != CmdArgType.DevVoid and \ - config.out_type != CmdArgType.DevVoid and \ - config.in_type != config.out_type: - raise RuntimeError("Commands with different in and out dtypes are not supported") - array, dtype, descr = get_pyton_type(config.in_type if config.in_type != CmdArgType.DevVoid else config.out_type) - tr_dtype[tr_name] = AttrDataFormat.SPECTRUM \ - if array else AttrDataFormat.SCALAR, dtype, descr + if ( + config.in_type != CmdArgType.DevVoid + and config.out_type != CmdArgType.DevVoid + and config.in_type != config.out_type + ): + raise RuntimeError( + "Commands with different in and out dtypes are not supported" + ) + array, dtype, descr = get_pyton_type( + config.in_type + if config.in_type != CmdArgType.DevVoid + else config.out_type + ) + tr_dtype[tr_name] = ( + AttrDataFormat.SPECTRUM if array else AttrDataFormat.SCALAR, + dtype, + descr, + ) else: raise RuntimeError(f"Unknown config type: {type(config)}") tr_format, tr_dtype, tr_dtype_desc = get_unique(tr_dtype, "typeids") - # tango commands are limited in functionality: they do not have info about shape and Enum labels + # tango commands are limited in functionality: + # they do not have info about shape and Enum labels trl_config = list(tr_configs.values())[0] max_x = trl_config.max_dim_x if hasattr(trl_config, "max_dim_x") else np.Inf max_y = trl_config.max_dim_y if hasattr(trl_config, "max_dim_y") else np.Inf @@ -312,7 +350,9 @@ def get_trl_descriptor(datatype: Optional[Type], tango_resource: str, # Check we wanted an array of this type dtype = get_dtype_extended(datatype) if not dtype: - raise TypeError(f"{tango_resource} has type [{tr_dtype}] not {datatype.__name__}") + raise TypeError( + f"{tango_resource} has type [{tr_dtype}] not {datatype.__name__}" + ) if dtype != tr_dtype: raise TypeError(f"{tango_resource} has type [{tr_dtype}] not [{dtype}]") @@ -328,21 +368,32 @@ def get_trl_descriptor(datatype: Optional[Type], tango_resource: str, if datatype: if not issubclass(datatype, (Enum, DevState)): - raise TypeError(f"{tango_resource} has type Enum not {datatype.__name__}") + raise TypeError( + f"{tango_resource} has type Enum not {datatype.__name__}" + ) if tr_dtype == Enum and is_attr: choices = tuple(v.name for v in datatype) if set(choices) != set(trl_choices): - raise TypeError(f"{tango_resource} has choices {trl_choices} not {choices}") - return dict(source=tango_resource, dtype="string", shape=[], choices=trl_choices) + raise TypeError( + f"{tango_resource} has choices {trl_choices} not {choices}" + ) + return dict( + source=tango_resource, dtype="string", shape=[], choices=trl_choices + ) else: if datatype and not issubclass(tr_dtype, datatype): - raise TypeError(f"{tango_resource} has type {tr_dtype.__name__} not {datatype.__name__}") + raise TypeError( + f"{tango_resource} has type {tr_dtype.__name__} " + f"not {datatype.__name__}" + ) return dict(source=tango_resource, dtype=tr_dtype_desc, shape=[]) # -------------------------------------------------------------------- -async def get_tango_trl(full_trl: str, device_proxy: Optional[DeviceProxy]) -> TangoProxy: - device_trl, trl_name = full_trl.rsplit('/', 1) +async def get_tango_trl( + full_trl: str, device_proxy: Optional[DeviceProxy] +) -> TangoProxy: + device_trl, trl_name = full_trl.rsplit("/", 1) trl_name = trl_name.lower() device_proxy = device_proxy or await DeviceProxy(device_trl) @@ -359,7 +410,7 @@ async def get_tango_trl(full_trl: str, device_proxy: Optional[DeviceProxy]) -> T # all pipes can be always accessible with low register all_pipes = [pipe_name.lower() for pipe_name in device_proxy.get_pipe_list()] if trl_name in all_pipes: - raise NotImplemented("Pipes are not supported") + raise NotImplementedError("Pipes are not supported") raise RuntimeError(f"{trl_name} cannot be found in {device_proxy.name()}") @@ -367,15 +418,20 @@ async def get_tango_trl(full_trl: str, device_proxy: Optional[DeviceProxy]) -> T # -------------------------------------------------------------------- class TangoTransport(TangoSignalBackend[T]): - def __init__(self, - datatype: Optional[Type[T]], - read_trl: str, - write_trl: str, - device_proxy: Optional[DeviceProxy] = None): + def __init__( + self, + datatype: Optional[Type[T]], + read_trl: str, + write_trl: str, + device_proxy: Optional[DeviceProxy] = None, + ): self.datatype = datatype self.read_trl = read_trl self.write_trl = write_trl - self.proxies: Dict[str, TangoProxy] = {read_trl: device_proxy, write_trl: device_proxy} + self.proxies: Dict[str, TangoProxy] = { + read_trl: device_proxy, + write_trl: device_proxy, + } self.trl_configs: Dict[str, AttributeInfoEx] = {} self.source = f"{self.read_trl}" self.descriptor: Descriptor = {} # type: ignore @@ -400,7 +456,9 @@ async def connect(self, timeout: float = DEFAULT_TIMEOUT): else: # The same, so only need to connect one await self._connect_and_store_config(self.read_trl) - self.descriptor = get_trl_descriptor(self.datatype, self.read_trl, self.trl_configs) + self.descriptor = get_trl_descriptor( + self.datatype, self.read_trl, self.trl_configs + ) # -------------------------------------------------------------------- async def put(self, write_value: Optional[T], wait=True, timeout=None): @@ -428,15 +486,21 @@ def is_cachable(self): # -------------------------------------------------------------------- def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: - assert self.proxies[self.read_trl].support_events, f"{self.source} does not support events" + assert self.proxies[ + self.read_trl + ].support_events, f"{self.source} does not support events" if callback: - assert (not self.proxies[self.read_trl].has_subscription()), "Cannot set a callback when one is already set" + assert not self.proxies[ + self.read_trl + ].has_subscription(), "Cannot set a callback when one is already set" try: self.proxies[self.read_trl].subscribe_callback(callback) - except Exception as err: - raise RuntimeError(f"Cannot set event for {self.read_trl}. " - f"This signal should be used only as non-cached!") + except AssertionError or RuntimeError: + raise RuntimeError( + f"Cannot set event for {self.read_trl}. " + f"This signal should be used only as non-cached!" + ) else: self.proxies[self.read_trl].unsubscribe_callback() diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index 1cd17dfb72..32a06414e7 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -6,9 +6,9 @@ from tango.asyncio import DeviceProxy -from ophyd_async.core import StandardReadable, AsyncStatus +from ophyd_async.core import AsyncStatus, StandardReadable -__all__ = ("TangoReadableDevice", ) +__all__ = ("TangoReadableDevice",) # -------------------------------------------------------------------- diff --git a/src/ophyd_async/tango/device_controllers/__init__.py b/src/ophyd_async/tango/device_controllers/__init__.py index 00cac79f68..59cf2d18f6 100644 --- a/src/ophyd_async/tango/device_controllers/__init__.py +++ b/src/ophyd_async/tango/device_controllers/__init__.py @@ -1,7 +1,5 @@ -from ophyd_async.tango.device_controllers.omsvme58 import OmsVME58Motor from ophyd_async.tango.device_controllers.dgg2 import DGG2Timer +from ophyd_async.tango.device_controllers.omsvme58 import OmsVME58Motor from ophyd_async.tango.device_controllers.sis3820 import SIS3820Counter -__all__ = ("OmsVME58Motor", - "SIS3820Counter", - "DGG2Timer") +__all__ = ("OmsVME58Motor", "SIS3820Counter", "DGG2Timer") diff --git a/src/ophyd_async/tango/device_controllers/dgg2.py b/src/ophyd_async/tango/device_controllers/dgg2.py index 4164882566..70abd7eb88 100644 --- a/src/ophyd_async/tango/device_controllers/dgg2.py +++ b/src/ophyd_async/tango/device_controllers/dgg2.py @@ -1,11 +1,9 @@ - from __future__ import annotations -import asyncio import time -from typing import List, Callable +from typing import Callable, List -from bluesky.protocols import Triggerable, Preparable +from bluesky.protocols import Preparable, Triggerable from ophyd_async.core import AsyncStatus from ophyd_async.tango import TangoReadableDevice, tango_signal_rw, tango_signal_x @@ -16,22 +14,35 @@ class DGG2Timer(TangoReadableDevice, Triggerable, Preparable): # -------------------------------------------------------------------- def __init__(self, trl: str, name="") -> None: + self.sampletime = None + self.remainingtime = None + self.startandwaitfortimer = None + TangoReadableDevice.__init__(self, trl, name) self._set_success = True # -------------------------------------------------------------------- def register_signals(self): - self.sampletime = tango_signal_rw(float, self.trl + '/sampletime', device_proxy=self.proxy) - self.remainingtime = tango_signal_rw(float, self.trl + '/remainingtime', device_proxy=self.proxy) + self.sampletime = tango_signal_rw( + float, self.trl + "/sampletime", device_proxy=self.proxy + ) + self.remainingtime = tango_signal_rw( + float, self.trl + "/remainingtime", device_proxy=self.proxy + ) - self.set_readable_signals(read_uncached=[self.sampletime], - config=[self.sampletime]) + self.set_readable_signals( + read_uncached=[self.sampletime], config=[self.sampletime] + ) - self.startandwaitfortimer = tango_signal_x(self.trl+'/startandwaitfortimer', device_proxy=self.proxy) + self.startandwaitfortimer = tango_signal_x( + self.trl + "/startandwaitfortimer", device_proxy=self.proxy + ) # -------------------------------------------------------------------- - async def _trigger(self, watchers: List[Callable] = []): + async def _trigger(self, watchers: List[Callable] = None): + if watchers is None: + watchers = [] self._set_success = True start = time.monotonic() total_time = await self.sampletime.get_value() @@ -66,9 +77,9 @@ def trigger(self) -> AsyncStatus: return AsyncStatus(self._trigger(watchers), watchers) # -------------------------------------------------------------------- - def prepare(self, time) -> AsyncStatus: - return self.sampletime.set(time) + def prepare(self, p_time) -> AsyncStatus: + return self.sampletime.set(p_time) # -------------------------------------------------------------------- - async def set_time(self, time: float) -> None: - await self.sampletime.set(time) + async def set_time(self, s_time: float) -> None: + await self.sampletime.set(s_time) diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index 790a60bea7..8e27cf66ec 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -1,17 +1,19 @@ - from __future__ import annotations import asyncio import time +from typing import Callable, List, Optional -from typing import Optional, List, Callable - -from bluesky.protocols import Locatable, Stoppable, Location - +from bluesky.protocols import Locatable, Location, Stoppable from tango import DevState -from ophyd_async.tango import TangoReadableDevice, tango_signal_x, tango_signal_r, tango_signal_rw from ophyd_async.core import AsyncStatus +from ophyd_async.tango import ( + TangoReadableDevice, + tango_signal_r, + tango_signal_rw, + tango_signal_x, +) # -------------------------------------------------------------------- @@ -25,20 +27,29 @@ def __init__(self, trl: str, name="") -> None: # -------------------------------------------------------------------- def register_signals(self): - self.position = tango_signal_rw(float, self.trl + '/position', device_proxy=self.proxy) - self.baserate = tango_signal_rw(int, self.trl + '/baserate', device_proxy=self.proxy) - self.slewrate = tango_signal_rw(int, self.trl + '/slewrate', device_proxy=self.proxy) - self.conversion = tango_signal_rw(float, self.trl + '/conversion', device_proxy=self.proxy) - self.acceleration = tango_signal_rw(int, self.trl + '/acceleration', device_proxy=self.proxy) - - self.set_readable_signals(read_uncached=[self.position], - config=[self.baserate, - self.slewrate, - self.conversion, - self.acceleration]) - - self._stop = tango_signal_x(self.trl + '/stopmove', self.proxy) - self._state = tango_signal_r(DevState, self.trl + '/state', self.proxy) + self.position = tango_signal_rw( + float, self.trl + "/position", device_proxy=self.proxy + ) + self.baserate = tango_signal_rw( + int, self.trl + "/baserate", device_proxy=self.proxy + ) + self.slewrate = tango_signal_rw( + int, self.trl + "/slewrate", device_proxy=self.proxy + ) + self.conversion = tango_signal_rw( + float, self.trl + "/conversion", device_proxy=self.proxy + ) + self.acceleration = tango_signal_rw( + int, self.trl + "/acceleration", device_proxy=self.proxy + ) + + self.set_readable_signals( + read_uncached=[self.position], + config=[self.baserate, self.slewrate, self.conversion, self.acceleration], + ) + + self._stop = tango_signal_x(self.trl + "/stopmove", self.proxy) + self._state = tango_signal_r(DevState, self.trl + "/state", self.proxy) # -------------------------------------------------------------------- async def _move(self, new_position: float, watchers: List[Callable] = []): @@ -91,4 +102,3 @@ async def stop(self, success=False): # Put with completion will never complete as we are waiting for completion on # the move above, so need to pass wait=False await self._stop.trigger() - diff --git a/src/ophyd_async/tango/device_controllers/sis3820.py b/src/ophyd_async/tango/device_controllers/sis3820.py index fb7a140076..8700632560 100644 --- a/src/ophyd_async/tango/device_controllers/sis3820.py +++ b/src/ophyd_async/tango/device_controllers/sis3820.py @@ -1,4 +1,3 @@ - from __future__ import annotations from typing import Dict @@ -20,13 +19,16 @@ def __init__(self, trl: str, name="") -> None: # -------------------------------------------------------------------- def register_signals(self): - self.counts = tango_signal_rw(float, self.trl + '/counts', device_proxy=self.proxy) - self.offset = tango_signal_rw(float, self.trl + '/offset', device_proxy=self.proxy) + self.counts = tango_signal_rw( + float, self.trl + "/counts", device_proxy=self.proxy + ) + self.offset = tango_signal_rw( + float, self.trl + "/offset", device_proxy=self.proxy + ) - self.set_readable_signals(read_uncached=[self.counts], - config=[self.offset]) + self.set_readable_signals(read_uncached=[self.counts], config=[self.offset]) - self.reset = tango_signal_x(self.trl + '/reset', device_proxy=self.proxy) + self.reset = tango_signal_x(self.trl + "/reset", device_proxy=self.proxy) # -------------------------------------------------------------------- # Theoretically counter has to be reset before triggering, but I do not how to do it diff --git a/src/ophyd_async/tango/sardana/__init__.py b/src/ophyd_async/tango/sardana/__init__.py index 2ba08cb7b5..d68bafbe32 100644 --- a/src/ophyd_async/tango/sardana/__init__.py +++ b/src/ophyd_async/tango/sardana/__init__.py @@ -1,3 +1,3 @@ from ophyd_async.tango.sardana.motor import SardanaMotor -__all__ = ("SardanaMotor",) \ No newline at end of file +__all__ = ("SardanaMotor",) diff --git a/src/ophyd_async/tango/sardana/motor.py b/src/ophyd_async/tango/sardana/motor.py index c79ddd9a3d..942b896851 100644 --- a/src/ophyd_async/tango/sardana/motor.py +++ b/src/ophyd_async/tango/sardana/motor.py @@ -1,17 +1,19 @@ - from __future__ import annotations import asyncio import time +from typing import Callable, List, Optional -from typing import Optional, List, Callable - -from bluesky.protocols import Locatable, Stoppable, Location - +from bluesky.protocols import Locatable, Location, Stoppable from tango import DevState -from ophyd_async.tango import TangoReadableDevice, tango_signal_x, tango_signal_r, tango_signal_rw, tango_signal_w -from ophyd_async.core import AsyncStatus, Signal +from ophyd_async.core import AsyncStatus +from ophyd_async.tango import ( + TangoReadableDevice, + tango_signal_r, + tango_signal_rw, + tango_signal_x, +) # -------------------------------------------------------------------- @@ -25,23 +27,34 @@ def __init__(self, trl: str, name="") -> None: # -------------------------------------------------------------------- def register_signals(self): - self.position = tango_signal_rw(float, self.trl + '/Position', device_proxy=self.proxy) - self.baserate = tango_signal_rw(float, self.trl + '/Base_rate', device_proxy=self.proxy) - self.velocity = tango_signal_rw(float, self.trl + '/Velocity', device_proxy=self.proxy) - self.acceleration = tango_signal_rw(float, self.trl + '/Acceleration', device_proxy=self.proxy) - self.deceleration = tango_signal_rw(float, self.trl + '/Deceleration', device_proxy=self.proxy) - - self.set_readable_signals(read_uncached=[self.position], - config=[self.baserate, - self.velocity, - self.acceleration, - self.deceleration]) - - self._stop = tango_signal_x(self.trl + '/Stop', self.proxy) - self._state = tango_signal_r(DevState, self.trl + '/State', self.proxy) + self.position = tango_signal_rw( + float, self.trl + "/Position", device_proxy=self.proxy + ) + self.baserate = tango_signal_rw( + float, self.trl + "/Base_rate", device_proxy=self.proxy + ) + self.velocity = tango_signal_rw( + float, self.trl + "/Velocity", device_proxy=self.proxy + ) + self.acceleration = tango_signal_rw( + float, self.trl + "/Acceleration", device_proxy=self.proxy + ) + self.deceleration = tango_signal_rw( + float, self.trl + "/Deceleration", device_proxy=self.proxy + ) + + self.set_readable_signals( + read_uncached=[self.position], + config=[self.baserate, self.velocity, self.acceleration, self.deceleration], + ) + + self._stop = tango_signal_x(self.trl + "/Stop", self.proxy) + self._state = tango_signal_r(DevState, self.trl + "/State", self.proxy) # -------------------------------------------------------------------- - async def _move(self, new_position: float, watchers: List[Callable] = []): + async def _move(self, new_position: float, watchers: List[Callable] = None): + if watchers is None: + watchers = [] self._set_success = True start = time.monotonic() start_position = await self.position.get_value() @@ -84,4 +97,3 @@ async def stop(self, success=False): # Put with completion will never complete as we are waiting for completion on # the move above, so need to pass wait=False await self._stop.execute(wait=False) - diff --git a/src/ophyd_async/tango/signal/__init__.py b/src/ophyd_async/tango/signal/__init__.py index d37672efdc..e006b3645c 100644 --- a/src/ophyd_async/tango/signal/__init__.py +++ b/src/ophyd_async/tango/signal/__init__.py @@ -1,9 +1,15 @@ -from .signal import tango_signal_r, tango_signal_rw, tango_signal_w, tango_signal_x, tango_signal_auto +from .signal import ( + tango_signal_auto, + tango_signal_r, + tango_signal_rw, + tango_signal_w, + tango_signal_x, +) __all__ = ( "tango_signal_r", "tango_signal_rw", "tango_signal_w", "tango_signal_x", - "tango_signal_auto" + "tango_signal_auto", ) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 8d76d58f84..3d36d3c87b 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -3,29 +3,37 @@ from __future__ import annotations from typing import Optional, Type, Union -from tango.asyncio import DeviceProxy -from tango import DeviceProxy as SyncDeviceProxy -from tango import AttrWriteType, CmdArgType +from tango import AttrWriteType, CmdArgType +from tango import DeviceProxy as SyncDeviceProxy +from tango.asyncio import DeviceProxy from ophyd_async.core import T - -from ophyd_async.tango._backend import (TangoTransport, TangoSignalRW, TangoSignalW, - TangoSignalX, TangoSignalR, TangoSignalBackend) - -__all__ = ("tango_signal_rw", - "tango_signal_r", - "tango_signal_w", - "tango_signal_x", - "tango_signal_auto") +from ophyd_async.tango._backend import ( + TangoSignalBackend, + TangoSignalR, + TangoSignalRW, + TangoSignalW, + TangoSignalX, + TangoTransport, +) + +__all__ = ( + "tango_signal_rw", + "tango_signal_r", + "tango_signal_w", + "tango_signal_x", + "tango_signal_auto", +) # -------------------------------------------------------------------- -def tango_signal_rw(datatype: Type[T], - read_trl: str, - write_trl: Optional[str] = None, - device_proxy: Optional[DeviceProxy] = None - ) -> TangoSignalRW[T]: +def tango_signal_rw( + datatype: Type[T], + read_trl: str, + write_trl: Optional[str] = None, + device_proxy: Optional[DeviceProxy] = None, +) -> TangoSignalRW[T]: """Create a `SignalRW` backed by 1 or 2 Tango Attribute/Command Parameters @@ -44,10 +52,9 @@ def tango_signal_rw(datatype: Type[T], # -------------------------------------------------------------------- -def tango_signal_r(datatype: Type[T], - read_trl: str, - device_proxy: Optional[DeviceProxy] = None - ) -> TangoSignalR[T]: +def tango_signal_r( + datatype: Type[T], read_trl: str, device_proxy: Optional[DeviceProxy] = None +) -> TangoSignalR[T]: """Create a `SignalR` backed by 1 Tango Attribute/Command Parameters @@ -64,10 +71,9 @@ def tango_signal_r(datatype: Type[T], # -------------------------------------------------------------------- -def tango_signal_w(datatype: Type[T], - write_trl: str, - device_proxy: Optional[DeviceProxy] = None - ) -> TangoSignalW[T]: +def tango_signal_w( + datatype: Type[T], write_trl: str, device_proxy: Optional[DeviceProxy] = None +) -> TangoSignalW[T]: """Create a `TangoSignalW` backed by 1 Tango Attribute/Command Parameters @@ -84,9 +90,9 @@ def tango_signal_w(datatype: Type[T], # -------------------------------------------------------------------- -def tango_signal_x(write_trl: str, - device_proxy: Optional[DeviceProxy] = None - ) -> TangoSignalX: +def tango_signal_x( + write_trl: str, device_proxy: Optional[DeviceProxy] = None +) -> TangoSignalX: """Create a `SignalX` backed by 1 Tango Attribute/Command Parameters @@ -96,16 +102,21 @@ def tango_signal_x(write_trl: str, device_proxy: If given, this DeviceProxy will be used """ - backend: TangoSignalBackend = TangoTransport(None, write_trl, write_trl, device_proxy) + backend: TangoSignalBackend = TangoTransport( + None, write_trl, write_trl, device_proxy + ) return TangoSignalX(backend) # -------------------------------------------------------------------- -def tango_signal_auto(datatype: Type[T], full_trl: str, device_proxy: Optional[DeviceProxy] = None) -> \ - Union[TangoSignalW, TangoSignalX, TangoSignalR, TangoSignalRW]: - backend: TangoSignalBackend = TangoTransport(datatype, full_trl, full_trl, device_proxy) - - device_trl, tr_name = full_trl.rsplit('/', 1) +def tango_signal_auto( + datatype: Type[T], full_trl: str, device_proxy: Optional[DeviceProxy] = None +) -> Union[TangoSignalW, TangoSignalX, TangoSignalR, TangoSignalRW]: + backend: TangoSignalBackend = TangoTransport( + datatype, full_trl, full_trl, device_proxy + ) + + device_trl, tr_name = full_trl.rsplit("/", 1) device_proxy = SyncDeviceProxy(device_trl) if tr_name in device_proxy.get_attribute_list(): config = device_proxy.get_attribute_config(tr_name) @@ -126,6 +137,6 @@ def tango_signal_auto(datatype: Type[T], full_trl: str, device_proxy: Optional[D return TangoSignalX(backend) if tr_name in device_proxy.get_pipe_list(): - raise NotImplemented("Pipes are not supported") + raise NotImplementedError("Pipes are not supported") raise RuntimeError(f"Cannot find {tr_name} in {device_trl}") diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 59cef794c4..417722bd20 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -1,24 +1,27 @@ import time - -import pytest - -from typing import Type from enum import Enum, IntEnum +from typing import Type import numpy as np - -from tango import AttrQuality, AttrDataFormat, AttrWriteType, DeviceProxy, DevState, CmdArgType -from tango.test_utils import assert_close -from tango.server import Device, attribute, command +import pytest +from bluesky import RunEngine +from bluesky.plans import count +from tango import ( + AttrDataFormat, + AttrQuality, + AttrWriteType, + CmdArgType, + DeviceProxy, + DevState, +) from tango.asyncio_executor import set_global_executor +from tango.server import Device, attribute from tango.test_context import MultiDeviceTestContext +from tango.test_utils import assert_close -from ophyd_async.tango._backend._tango_transport import get_pyton_type -from ophyd_async.tango import TangoReadableDevice, tango_signal_auto from ophyd_async.core import DeviceCollector, T - -from bluesky import RunEngine -from bluesky.plans import count +from ophyd_async.tango import TangoReadableDevice, tango_signal_auto +from ophyd_async.tango._backend._tango_transport import get_pyton_type class TestEnum(IntEnum): @@ -37,8 +40,7 @@ class TestEnum(IntEnum): class TestDevice(Device): __test__ = False - _array = [[1, 2, 3], - [4, 5, 6]] + _array = [[1, 2, 3], [4, 5, 6]] _limitedvalue = 3 @@ -46,17 +48,29 @@ class TestDevice(Device): def justvalue(self): return 5 - @attribute(dtype=float, access=AttrWriteType.READ_WRITE, - dformat=AttrDataFormat.IMAGE, max_dim_x=3, max_dim_y=2) + @attribute( + dtype=float, + access=AttrWriteType.READ_WRITE, + dformat=AttrDataFormat.IMAGE, + max_dim_x=3, + max_dim_y=2, + ) def array(self) -> list[list[float]]: return self._array def write_array(self, array: list[list[float]]): self._array = array - @attribute(dtype=float, access=AttrWriteType.READ_WRITE, - min_value=0, min_alarm=1, min_warning=2, - max_warning=4, max_alarm=5, max_value=6) + @attribute( + dtype=float, + access=AttrWriteType.READ_WRITE, + min_value=0, + min_alarm=1, + min_warning=2, + max_warning=4, + max_alarm=5, + max_value=6, + ) def limitedvalue(self) -> float: return self._limitedvalue @@ -71,9 +85,13 @@ class TestReadableDevice(TangoReadableDevice): # -------------------------------------------------------------------- def register_signals(self): for name in TESTED_FEATURES: - setattr(self, name, tango_signal_auto(None, f"{self.trl}/{name}", self.proxy)) + setattr( + self, name, tango_signal_auto(None, f"{self.trl}/{name}", self.proxy) + ) - self.set_readable_signals(read_uncached=[getattr(self, name) for name in TESTED_FEATURES]) + self.set_readable_signals( + read_uncached=[getattr(self, name) for name in TESTED_FEATURES] + ) # -------------------------------------------------------------------- @@ -101,21 +119,31 @@ def describe_class(fqtrl): elif name in dev.get_command_list(): cmd_conf = dev.get_command_config(name) - _, _, descr = get_pyton_type(cmd_conf.in_type if cmd_conf.in_type != CmdArgType.DevVoid else cmd_conf.out_type) + _, _, descr = get_pyton_type( + cmd_conf.in_type + if cmd_conf.in_type != CmdArgType.DevVoid + else cmd_conf.out_type + ) is_array = False shape = [] value = getattr(dev, name)() else: - raise RuntimeError(f"Cannot find {name} in attributes/commands (pipes are not supported!)") - - description[f"test_device-{name}"] = {'source': f'{fqtrl}/{name}', # type: ignore - 'dtype': 'array' if is_array else descr, - 'shape': shape} - - values[f"test_device-{name}"] = {'value': value, - 'timestamp': pytest.approx(time.time()), - 'alarm_severity': AttrQuality.ATTR_VALID} + raise RuntimeError( + f"Cannot find {name} in attributes/commands (pipes are not supported!)" + ) + + description[f"test_device-{name}"] = { + "source": f"{fqtrl}/{name}", # type: ignore + "dtype": "array" if is_array else descr, + "shape": shape, + } + + values[f"test_device-{name}"] = { + "value": value, + "timestamp": pytest.approx(time.time()), + "alarm_severity": AttrQuality.ATTR_VALID, + } return values, description @@ -131,16 +159,23 @@ def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: if issubclass(python_type, DevState): return dict(dtype="string", shape=[], choices=list(DevState.names.keys())) if issubclass(python_type, Enum): - return dict(dtype="string", shape=[], choices=[] if is_cmd else [member.name for member in value.__class__]) + return dict( + dtype="string", + shape=[], + choices=[] if is_cmd else [member.name for member in value.__class__], + ) - return dict(dtype="array", shape=[np.Inf] if is_cmd else list(np.array(value).shape)) + return dict( + dtype="array", shape=[np.Inf] if is_cmd else list(np.array(value).shape) + ) # -------------------------------------------------------------------- @pytest.fixture(scope="module") def tango_test_device(): with MultiDeviceTestContext( - [{"class": TestDevice, "devices": [{"name": "test/device/1"}]}], process=True) as context: + [{"class": TestDevice, "devices": [{"name": "test/device/1"}]}], process=True + ) as context: yield context.get_device_access("test/device/1") diff --git a/tests/tango/test_ddg2.py b/tests/tango/test_ddg2.py index d8ae3fe482..a192ac16fd 100644 --- a/tests/tango/test_ddg2.py +++ b/tests/tango/test_ddg2.py @@ -1,14 +1,12 @@ -import pytest - from unittest.mock import Mock +import pytest +from bluesky import Msg, RunEngine +from bluesky.plans import count from tango.asyncio_executor import set_global_executor -from ophyd_async.tango.device_controllers import DGG2Timer from ophyd_async.core import DeviceCollector - -from bluesky import RunEngine, Msg -from bluesky.plans import count +from ophyd_async.tango.device_controllers import DGG2Timer # -------------------------------------------------------------------- @@ -25,7 +23,7 @@ async def dgg2(): yield dgg2 - + # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_connect(dgg2): @@ -47,12 +45,20 @@ async def test_readout_with_bluesky(dgg2): RE([Msg("prepare", dgg2, TEST_TIME)]) RE(count([dgg2], 3), readouts) - description = [args[0][1]['configuration'] for args in readouts.call_args_list if args[0][0] == "descriptor"][0] + description = [ + args[0][1]["configuration"] + for args in readouts.call_args_list + if args[0][0] == "descriptor" + ][0] assert "dgg2" in description - assert 'dgg2-sampletime' in description["dgg2"]["data"] - assert description["dgg2"]["data"]["dgg2-sampletime"] == pytest.approx(TEST_TIME, abs=0.1) - - readings = [args[0][1]['data'] for args in readouts.call_args_list if args[0][0] == "event"] + assert "dgg2-sampletime" in description["dgg2"]["data"] + assert description["dgg2"]["data"]["dgg2-sampletime"] == pytest.approx( + TEST_TIME, abs=0.1 + ) + + readings = [ + args[0][1]["data"] for args in readouts.call_args_list if args[0][0] == "event" + ] assert len(readings) == 3 assert "dgg2-sampletime" in readings[0] - assert readings[0]["dgg2-sampletime"] == pytest.approx(TEST_TIME, abs=0.1) \ No newline at end of file + assert readings[0]["dgg2-sampletime"] == pytest.approx(TEST_TIME, abs=0.1) diff --git a/tests/tango/test_motors.py b/tests/tango/test_motors.py index d1d1d972c9..ef0e027a8a 100644 --- a/tests/tango/test_motors.py +++ b/tests/tango/test_motors.py @@ -1,20 +1,16 @@ -import pytest import asyncio - -import numpy as np - from unittest.mock import Mock, call -from tango.asyncio_executor import set_global_executor - -from ophyd_async.tango.sardana import SardanaMotor -from ophyd_async.tango.device_controllers import OmsVME58Motor -from ophyd_async.core import DeviceCollector - +import numpy as np +import pytest from bluesky import RunEngine -from bluesky.plans import count, scan from bluesky.plan_stubs import mv +from bluesky.plans import count, scan +from tango.asyncio_executor import set_global_executor +from ophyd_async.core import DeviceCollector +from ophyd_async.tango.device_controllers import OmsVME58Motor +from ophyd_async.tango.sardana import SardanaMotor # Long enough for multiple asyncio event loop cycles to run so # all the tasks have a chance to run @@ -23,7 +19,7 @@ # dict: {class: tango trl} MOTORS_TO_TEST = { SardanaMotor: "motor/dummy_mot_ctrl/1", - OmsVME58Motor: "p09/motor/eh.01" + OmsVME58Motor: "p09/motor/eh.01", } @@ -88,7 +84,10 @@ async def test_move(dummy_motor): time_elapsed=pytest.approx(0.0, abs=0.05), ) await status - assert pytest.approx(target_position, abs=0.1) == await dummy_motor.position.get_value() + assert ( + pytest.approx(target_position, abs=0.1) + == await dummy_motor.position.get_value() + ) assert status.done done.assert_called_once_with(status) @@ -115,8 +114,17 @@ async def test_scan_motor_vs_motor_position(dummy_motor): RE(scan([dummy_motor.position], dummy_motor, 0, 1, num=11), readouts) assert readouts.call_count == 14 - assert set([args[0][0] for args in readouts.call_args_list]) == {'descriptor', 'event', 'start', 'stop'} - - positions = [args[0][1]['data']['dummy_motor-position'] for args in readouts.call_args_list if args[0][0] == "event"] + assert set([args[0][0] for args in readouts.call_args_list]) == { + "descriptor", + "event", + "start", + "stop", + } + + positions = [ + args[0][1]["data"]["dummy_motor-position"] + for args in readouts.call_args_list + if args[0][0] == "event" + ] for got, expected in zip(positions, np.arange(0, 1.1, 0.1)): - assert pytest.approx(got, abs=0.1) == expected \ No newline at end of file + assert pytest.approx(got, abs=0.1) == expected diff --git a/tests/tango/test_scans.py b/tests/tango/test_scans.py index 7161e099b7..0109c392a5 100644 --- a/tests/tango/test_scans.py +++ b/tests/tango/test_scans.py @@ -1,23 +1,23 @@ -import pytest -import asyncio +from unittest.mock import Mock import numpy as np - -from unittest.mock import Mock, call - -from tango.asyncio_executor import set_global_executor - -from ophyd_async.tango.device_controllers import OmsVME58Motor, DGG2Timer, SIS3820Counter -from ophyd_async.core import DeviceCollector - +import pytest from bluesky import RunEngine from bluesky.plans import scan +from tango.asyncio_executor import set_global_executor +from ophyd_async.core import DeviceCollector +from ophyd_async.tango.device_controllers import ( + DGG2Timer, + OmsVME58Motor, + SIS3820Counter, +) # Long enough for multiple asyncio event loop cycles to run so # all the tasks have a chance to run A_BIT = 0.001 + # -------------------------------------------------------------------- @pytest.fixture(autouse=True) def reset_tango_asyncio(): @@ -45,11 +45,23 @@ async def test_step_scan_motor_vs_counter_with_dgg2(devices_set): # now let's do some bluesky stuff RE = RunEngine() - RE(scan([omsvme58_motor, sis3820, dgg2timer], omsvme58_motor, 0, 1, num=11), readouts) + RE( + scan([omsvme58_motor, sis3820, dgg2timer], omsvme58_motor, 0, 1, num=11), + readouts, + ) assert readouts.call_count == 14 - assert set([args[0][0] for args in readouts.call_args_list]) == {'descriptor', 'event', 'start', 'stop'} - - positions = [args[0][1]['data']['omsvme58_motor-position'] for args in readouts.call_args_list if args[0][0] == "event"] + assert set([args[0][0] for args in readouts.call_args_list]) == { + "descriptor", + "event", + "start", + "stop", + } + + positions = [ + args[0][1]["data"]["omsvme58_motor-position"] + for args in readouts.call_args_list + if args[0][0] == "event" + ] for got, expected in zip(positions, np.arange(0, 1.1, 0.1)): - assert pytest.approx(got, abs=0.1) == expected \ No newline at end of file + assert pytest.approx(got, abs=0.1) == expected diff --git a/tests/tango/test_sis3820.py b/tests/tango/test_sis3820.py index 057a4532b3..b01c787b87 100644 --- a/tests/tango/test_sis3820.py +++ b/tests/tango/test_sis3820.py @@ -1,14 +1,12 @@ -import pytest - from unittest.mock import Mock +import pytest +from bluesky import RunEngine +from bluesky.plans import count from tango.asyncio_executor import set_global_executor -from ophyd_async.tango.device_controllers import SIS3820Counter from ophyd_async.core import DeviceCollector - -from bluesky import RunEngine -from bluesky.plans import count +from ophyd_async.tango.device_controllers import SIS3820Counter # -------------------------------------------------------------------- @@ -43,10 +41,16 @@ async def test_readout_with_bluesky(sis3820): RE = RunEngine() RE(count([sis3820], 3), readouts) - description = [args[0][1]['configuration'] for args in readouts.call_args_list if args[0][0] == "descriptor"][0] + description = [ + args[0][1]["configuration"] + for args in readouts.call_args_list + if args[0][0] == "descriptor" + ][0] assert "sis3820" in description - assert 'sis3820-offset' in description["sis3820"]["data"] + assert "sis3820-offset" in description["sis3820"]["data"] - readings = [args[0][1]['data'] for args in readouts.call_args_list if args[0][0] == "event"] + readings = [ + args[0][1]["data"] for args in readouts.call_args_list if args[0][0] == "event" + ] assert len(readings) == 3 - assert "sis3820-counts" in readings[0] \ No newline at end of file + assert "sis3820-counts" in readings[0] diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index 9ec1dd0464..7922e3ac5f 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -1,27 +1,22 @@ -import pytest import asyncio -import time import textwrap - -import numpy as np -import numpy.typing as npt - +import time +from enum import Enum, IntEnum from random import choice from typing import Any, Optional, Tuple, Type -from enum import Enum, IntEnum - -from tango import AttrWriteType, AttrDataFormat, DeviceProxy, DevState +import numpy as np +import numpy.typing as npt +import pytest +from bluesky.protocols import Reading +from tango import AttrDataFormat, AttrWriteType, DeviceProxy, DevState +from tango.asyncio_executor import set_global_executor from tango.server import Device, attribute, command -from tango.test_utils import assert_close from tango.test_context import MultiDeviceTestContext -from tango.asyncio_executor import set_global_executor - -from bluesky.protocols import Reading +from tango.test_utils import assert_close from ophyd_async.core import SignalBackend, T -from ophyd_async.tango._backend import TangoTransport, TangoSignalBackend - +from ophyd_async.tango._backend import TangoSignalBackend, TangoTransport # -------------------------------------------------------------------- """ @@ -38,18 +33,18 @@ class TestEnum(IntEnum): BASE_TYPES_SET = ( # type_name, tango_name, py_type, sample_values - ("boolean", 'DevBoolean', bool, (True, False)), - ("short", 'DevShort', int, (1, 2, 3, 4, 5)), - ("ushort", 'DevUShort', int, (1, 2, 3, 4, 5)), - ("long", 'DevLong', int, (1, 2, 3, 4, 5)), - ("ulong", 'DevULong', int, (1, 2, 3, 4, 5)), - ("long64", 'DevLong64', int, (1, 2, 3, 4, 5)), - ("char", 'DevUChar', int, (1, 2, 3, 4, 5)), - ("float", 'DevFloat', float, (1.1, 2.2, 3.3, 4.4, 5.5)), - ("double", 'DevDouble', float, (1.1, 2.2, 3.3, 4.4, 5.5)), - ("string", 'DevString', str, ('aaa', 'bbb', 'ccc')), - ("state", 'DevState', DevState, (DevState.ON, DevState.MOVING, DevState.ALARM)), - ("enum", 'DevEnum', TestEnum, (TestEnum.A, TestEnum.B)), + ("boolean", "DevBoolean", bool, (True, False)), + ("short", "DevShort", int, (1, 2, 3, 4, 5)), + ("ushort", "DevUShort", int, (1, 2, 3, 4, 5)), + ("long", "DevLong", int, (1, 2, 3, 4, 5)), + ("ulong", "DevULong", int, (1, 2, 3, 4, 5)), + ("long64", "DevLong64", int, (1, 2, 3, 4, 5)), + ("char", "DevUChar", int, (1, 2, 3, 4, 5)), + ("float", "DevFloat", float, (1.1, 2.2, 3.3, 4.4, 5.5)), + ("double", "DevDouble", float, (1.1, 2.2, 3.3, 4.4, 5.5)), + ("string", "DevString", str, ("aaa", "bbb", "ccc")), + ("state", "DevState", DevState, (DevState.ON, DevState.MOVING, DevState.ALARM)), + ("enum", "DevEnum", TestEnum, (TestEnum.A, TestEnum.B)), # ("encoded", 'DevEncoded', TestEnum, (TestEnum.A, TestEnum.B)), ) @@ -57,27 +52,67 @@ class TestEnum(IntEnum): COMMANDS_SET = [] for type_name, tango_type_name, py_type, values in BASE_TYPES_SET: - ATTRIBUTES_SET.extend([ - (f"{type_name}_scalar_attr", tango_type_name, AttrDataFormat.SCALAR, py_type, choice(values), choice(values)), - (f"{type_name}_spectrum_attr", tango_type_name, AttrDataFormat.SPECTRUM, npt.NDArray[py_type], - [choice(values), choice(values), choice(values)], [choice(values), choice(values), choice(values)]), - (f"{type_name}_image_attr", tango_type_name, AttrDataFormat.IMAGE, npt.NDArray[py_type], - [[choice(values), choice(values), choice(values)], [choice(values), choice(values), choice(values)]], - [[choice(values), choice(values), choice(values)], [choice(values), choice(values), choice(values)]]) - ]) - - if tango_type_name == 'DevUChar': + ATTRIBUTES_SET.extend( + [ + ( + f"{type_name}_scalar_attr", + tango_type_name, + AttrDataFormat.SCALAR, + py_type, + choice(values), + choice(values), + ), + ( + f"{type_name}_spectrum_attr", + tango_type_name, + AttrDataFormat.SPECTRUM, + npt.NDArray[py_type], + [choice(values), choice(values), choice(values)], + [choice(values), choice(values), choice(values)], + ), + ( + f"{type_name}_image_attr", + tango_type_name, + AttrDataFormat.IMAGE, + npt.NDArray[py_type], + [ + [choice(values), choice(values), choice(values)], + [choice(values), choice(values), choice(values)], + ], + [ + [choice(values), choice(values), choice(values)], + [choice(values), choice(values), choice(values)], + ], + ), + ] + ) + + if tango_type_name == "DevUChar": continue else: - COMMANDS_SET.append((f"{type_name}_scalar_cmd", tango_type_name, AttrDataFormat.SCALAR, - py_type, choice(values), choice(values))) - if tango_type_name in ['DevState', 'DevEnum']: + COMMANDS_SET.append( + ( + f"{type_name}_scalar_cmd", + tango_type_name, + AttrDataFormat.SCALAR, + py_type, + choice(values), + choice(values), + ) + ) + if tango_type_name in ["DevState", "DevEnum"]: continue else: - COMMANDS_SET.append((f"{type_name}_spectrum_cmd", tango_type_name, AttrDataFormat.SPECTRUM, - npt.NDArray[py_type], - [choice(values), choice(values), choice(values)], - [choice(values), choice(values), choice(values)])) + COMMANDS_SET.append( + ( + f"{type_name}_spectrum_cmd", + tango_type_name, + AttrDataFormat.SPECTRUM, + npt.NDArray[py_type], + [choice(values), choice(values), choice(values)], + [choice(values), choice(values), choice(values)], + ) + ) # -------------------------------------------------------------------- @@ -97,7 +132,7 @@ def initialize_dynamic_attributes(self): fset=self.write, max_dim_x=3, max_dim_y=2, - enum_labels=[member.name for member in TestEnum] + enum_labels=[member.name for member in TestEnum], ) self.add_attribute(attr) self.set_change_event(name, True, False) @@ -108,7 +143,7 @@ def initialize_dynamic_attributes(self): dtype_in=typ, dformat_in=form, dtype_out=typ, - dformat_out=form + dformat_out=form, ) self.add_command(cmd) @@ -146,7 +181,8 @@ def assert_enum(initial_value, readout_value): @pytest.fixture(scope="module") def echo_device(): with MultiDeviceTestContext( - [{"class": EchoDevice, "devices": [{"name": "test/device/1"}]}], process=True) as context: + [{"class": EchoDevice, "devices": [{"name": "test/device/1"}]}], process=True + ) as context: yield context.get_device_access("test/device/1") @@ -169,13 +205,21 @@ def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: if issubclass(python_type, DevState): return dict(dtype="string", shape=[], choices=list(DevState.names.keys())) if issubclass(python_type, Enum): - return dict(dtype="string", shape=[], choices=[] if is_cmd else [member.name for member in value.__class__]) - - return dict(dtype="array", shape=[np.Inf] if is_cmd else list(np.array(value).shape)) + return dict( + dtype="string", + shape=[], + choices=[] if is_cmd else [member.name for member in value.__class__], + ) + + return dict( + dtype="array", shape=[np.Inf] if is_cmd else list(np.array(value).shape) + ) # -------------------------------------------------------------------- -async def make_backend(typ: Optional[Type], pv: str, connect=True) -> TangoSignalBackend: +async def make_backend( + typ: Optional[Type], pv: str, connect=True +) -> TangoSignalBackend: backend = TangoTransport(typ, pv, pv) if connect: await asyncio.wait_for(backend.connect(), 10) @@ -226,15 +270,15 @@ def close(self): # -------------------------------------------------------------------- async def assert_monitor_then_put( - echo_device: str, - pv: str, - initial_value: T, - put_value: T, - descriptor: dict, - datatype: Optional[Type[T]] = None, + echo_device: str, + pv: str, + initial_value: T, + put_value: T, + descriptor: dict, + datatype: Optional[Type[T]] = None, ): prepare_device(echo_device, pv, initial_value) - source = echo_device + '/' + pv + source = echo_device + "/" + pv backend = await make_backend(datatype, source) # Make a monitor queue that will monitor for updates q = MonitorQueue(backend) @@ -252,31 +296,39 @@ async def assert_monitor_then_put( # -------------------------------------------------------------------- @pytest.mark.asyncio -@pytest.mark.parametrize("pv, tango_type, d_format, py_type, initial_value, put_value", ATTRIBUTES_SET, - ids=[x[0] for x in ATTRIBUTES_SET]) -async def test_backend_get_put_monitor_attr(echo_device: str, - pv: str, - tango_type: str, - d_format: AttrDataFormat, - py_type: Type[T], - initial_value: T, - put_value: T): +@pytest.mark.parametrize( + "pv, tango_type, d_format, py_type, initial_value, put_value", + ATTRIBUTES_SET, + ids=[x[0] for x in ATTRIBUTES_SET], +) +async def test_backend_get_put_monitor_attr( + echo_device: str, + pv: str, + tango_type: str, + d_format: AttrDataFormat, + py_type: Type[T], + initial_value: T, + put_value: T, +): # With the given datatype, check we have the correct initial value and putting works descriptor = get_test_descriptor(py_type, initial_value, False) - await assert_monitor_then_put(echo_device, pv, initial_value, put_value, descriptor, py_type) + await assert_monitor_then_put( + echo_device, pv, initial_value, put_value, descriptor, py_type + ) # # With guessed datatype, check we can set it back to the initial value - # await assert_monitor_then_put(echo_device, pv, initial_value, put_value, descriptor) + # await assert_monitor_then_put(echo_device, pv, initial_value, put_value, + # descriptor) # -------------------------------------------------------------------- async def assert_put_read( - echo_device: str, - pv: str, - put_value: T, - descriptor: dict, - datatype: Optional[Type[T]] = None, + echo_device: str, + pv: str, + put_value: T, + descriptor: dict, + datatype: Optional[Type[T]] = None, ): - source = echo_device + '/' + pv + source = echo_device + "/" + pv backend = await make_backend(datatype, source) # Make a monitor queue that will monitor for updates assert dict(source=source, **descriptor) == await backend.get_descriptor() @@ -291,24 +343,29 @@ async def assert_put_read( assert_close(await backend.get_value(), put_value) get_reading = dict(await backend.get_reading()) - get_reading.pop('value') + get_reading.pop("value") assert expected_reading == get_reading # -------------------------------------------------------------------- @pytest.mark.asyncio -@pytest.mark.parametrize("pv, tango_type, d_format, py_type, initial_value, put_value", COMMANDS_SET, - ids=[x[0] for x in COMMANDS_SET]) -async def test_backend_get_put_monitor_cmd(echo_device: str, - pv: str, - tango_type: str, - d_format: AttrDataFormat, - py_type: Type[T], - initial_value: T, - put_value: T): +@pytest.mark.parametrize( + "pv, tango_type, d_format, py_type, initial_value, put_value", + COMMANDS_SET, + ids=[x[0] for x in COMMANDS_SET], +) +async def test_backend_get_put_monitor_cmd( + echo_device: str, + pv: str, + tango_type: str, + d_format: AttrDataFormat, + py_type: Type[T], + initial_value: T, + put_value: T, +): print("Starting test!") # With the given datatype, check we have the correct initial value and putting works descriptor = get_test_descriptor(py_type, initial_value, True) await assert_put_read(echo_device, pv, put_value, descriptor, py_type) # # With guessed datatype, check we can set it back to the initial value - await assert_put_read(echo_device, pv, put_value, descriptor) \ No newline at end of file + await assert_put_read(echo_device, pv, put_value, descriptor) From 571948976ce3ea10a1874b1a3b10b89526073b8a Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 27 Feb 2024 15:04:01 +0100 Subject: [PATCH 016/141] reversed some changes which caused mypy errors --- src/ophyd_async/tango/device_controllers/dgg2.py | 8 +------- src/ophyd_async/tango/sardana/motor.py | 4 +--- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/ophyd_async/tango/device_controllers/dgg2.py b/src/ophyd_async/tango/device_controllers/dgg2.py index 70abd7eb88..f91ea0ccd8 100644 --- a/src/ophyd_async/tango/device_controllers/dgg2.py +++ b/src/ophyd_async/tango/device_controllers/dgg2.py @@ -14,10 +14,6 @@ class DGG2Timer(TangoReadableDevice, Triggerable, Preparable): # -------------------------------------------------------------------- def __init__(self, trl: str, name="") -> None: - self.sampletime = None - self.remainingtime = None - self.startandwaitfortimer = None - TangoReadableDevice.__init__(self, trl, name) self._set_success = True @@ -40,9 +36,7 @@ def register_signals(self): ) # -------------------------------------------------------------------- - async def _trigger(self, watchers: List[Callable] = None): - if watchers is None: - watchers = [] + async def _trigger(self, watchers: List[Callable] = []): self._set_success = True start = time.monotonic() total_time = await self.sampletime.get_value() diff --git a/src/ophyd_async/tango/sardana/motor.py b/src/ophyd_async/tango/sardana/motor.py index 942b896851..15e87ea603 100644 --- a/src/ophyd_async/tango/sardana/motor.py +++ b/src/ophyd_async/tango/sardana/motor.py @@ -52,9 +52,7 @@ def register_signals(self): self._state = tango_signal_r(DevState, self.trl + "/State", self.proxy) # -------------------------------------------------------------------- - async def _move(self, new_position: float, watchers: List[Callable] = None): - if watchers is None: - watchers = [] + async def _move(self, new_position: float, watchers: List[Callable] = []): self._set_success = True start = time.monotonic() start_position = await self.position.get_value() From d783ac2f2199752e58e96037b797b662b4c63525 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 27 Feb 2024 15:12:10 +0100 Subject: [PATCH 017/141] Fixed typos in some definitions. --- src/ophyd_async/tango/_backend/_signal_backend.py | 6 +++--- src/ophyd_async/tango/_backend/_tango_transport.py | 6 +++--- tests/tango/test_base_device.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_signal_backend.py b/src/ophyd_async/tango/_backend/_signal_backend.py index e93714dc11..a1b816d21a 100644 --- a/src/ophyd_async/tango/_backend/_signal_backend.py +++ b/src/ophyd_async/tango/_backend/_signal_backend.py @@ -8,7 +8,7 @@ # -------------------------------------------------------------------- # from tango attributes one can get setvalue, so we extend SignalRW and SignalW -class SignalWithSetpoit: +class SignalWithSetpoint: @add_timeout async def get_setpoint(self, cached: Optional[bool] = None) -> T: """The last written value to TRL""" @@ -24,11 +24,11 @@ def is_cachable(self) -> T: # -------------------------------------------------------------------- -class TangoSignalW(SignalW[T], CachableOrNot, SignalWithSetpoit): ... # noqa: E701 +class TangoSignalW(SignalW[T], CachableOrNot, SignalWithSetpoint): ... # noqa: E701 # -------------------------------------------------------------------- -class TangoSignalRW(SignalRW[T], CachableOrNot, SignalWithSetpoit): ... # noqa: E701 +class TangoSignalRW(SignalRW[T], CachableOrNot, SignalWithSetpoint): ... # noqa: E701 # -------------------------------------------------------------------- diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 8c15a37e2d..a1ff0d229e 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -69,7 +69,7 @@ def get_signal_auto(self) -> str: # -------------------------------------------------------------------- -def get_pyton_type(tango_type) -> tuple[bool, T, str]: +def get_python_type(tango_type) -> tuple[bool, T, str]: array = is_array(tango_type) if is_int(tango_type, True): return array, int, "integer" @@ -311,7 +311,7 @@ def get_trl_descriptor( tr_dtype = {} for tr_name, config in tr_configs.items(): if isinstance(config, AttributeInfoEx): - _, dtype, descr = get_pyton_type(config.data_type) + _, dtype, descr = get_python_type(config.data_type) tr_dtype[tr_name] = config.data_format, dtype, descr elif isinstance(config, CommandInfo): if ( @@ -322,7 +322,7 @@ def get_trl_descriptor( raise RuntimeError( "Commands with different in and out dtypes are not supported" ) - array, dtype, descr = get_pyton_type( + array, dtype, descr = get_python_type( config.in_type if config.in_type != CmdArgType.DevVoid else config.out_type diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 417722bd20..e88a0b72a4 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -21,7 +21,7 @@ from ophyd_async.core import DeviceCollector, T from ophyd_async.tango import TangoReadableDevice, tango_signal_auto -from ophyd_async.tango._backend._tango_transport import get_pyton_type +from ophyd_async.tango._backend._tango_transport import get_python_type class TestEnum(IntEnum): @@ -104,7 +104,7 @@ def describe_class(fqtrl): if name in dev.get_attribute_list(): attr_conf = dev.get_attribute_config(name) value = dev.read_attribute(name).value - _, _, descr = get_pyton_type(attr_conf.data_type) + _, _, descr = get_python_type(attr_conf.data_type) max_x = attr_conf.max_dim_x max_y = attr_conf.max_dim_y if attr_conf.data_format == AttrDataFormat.SCALAR: @@ -119,7 +119,7 @@ def describe_class(fqtrl): elif name in dev.get_command_list(): cmd_conf = dev.get_command_config(name) - _, _, descr = get_pyton_type( + _, _, descr = get_python_type( cmd_conf.in_type if cmd_conf.in_type != CmdArgType.DevVoid else cmd_conf.out_type From e978d757d1979c7ea637816187ebb624951231e5 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 15 Apr 2024 11:09:05 +0200 Subject: [PATCH 018/141] Changed signal names and added _stop method expected by the queueserver when it runs a scan --- src/ophyd_async/tango/device_controllers/omsvme58.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index 8e27cf66ec..9d6f13a2e8 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -48,8 +48,8 @@ def register_signals(self): config=[self.baserate, self.slewrate, self.conversion, self.acceleration], ) - self._stop = tango_signal_x(self.trl + "/stopmove", self.proxy) - self._state = tango_signal_r(DevState, self.trl + "/state", self.proxy) + self.stop_ = tango_signal_x(self.trl + "/stopmove", self.proxy) + self.state_ = tango_signal_r(DevState, self.trl + "/state", self.proxy) # -------------------------------------------------------------------- async def _move(self, new_position: float, watchers: List[Callable] = []): @@ -74,7 +74,7 @@ def update_watchers(current_position: float): try: await self.position.set(new_position) await asyncio.sleep(0.1) - while await self._state.get_value() == DevState.MOVING: + while await self.state_.get_value() == DevState.MOVING: await asyncio.sleep(0.1) finally: if self.position.is_cachable(): @@ -101,4 +101,6 @@ async def stop(self, success=False): self._set_success = success # Put with completion will never complete as we are waiting for completion on # the move above, so need to pass wait=False - await self._stop.trigger() + await self.stop_.trigger() + + _stop = stop From ebc32975882e10679e0632b132f751595a7d5187 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 15 Apr 2024 13:49:30 +0200 Subject: [PATCH 019/141] Reversed last commit --- src/ophyd_async/tango/device_controllers/omsvme58.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index 9d6f13a2e8..8e27cf66ec 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -48,8 +48,8 @@ def register_signals(self): config=[self.baserate, self.slewrate, self.conversion, self.acceleration], ) - self.stop_ = tango_signal_x(self.trl + "/stopmove", self.proxy) - self.state_ = tango_signal_r(DevState, self.trl + "/state", self.proxy) + self._stop = tango_signal_x(self.trl + "/stopmove", self.proxy) + self._state = tango_signal_r(DevState, self.trl + "/state", self.proxy) # -------------------------------------------------------------------- async def _move(self, new_position: float, watchers: List[Callable] = []): @@ -74,7 +74,7 @@ def update_watchers(current_position: float): try: await self.position.set(new_position) await asyncio.sleep(0.1) - while await self.state_.get_value() == DevState.MOVING: + while await self._state.get_value() == DevState.MOVING: await asyncio.sleep(0.1) finally: if self.position.is_cachable(): @@ -101,6 +101,4 @@ async def stop(self, success=False): self._set_success = success # Put with completion will never complete as we are waiting for completion on # the move above, so need to pass wait=False - await self.stop_.trigger() - - _stop = stop + await self._stop.trigger() From d1fbcba18efdf93ac1343fa701f91e97b55efd5f Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 18 Apr 2024 16:21:53 +0200 Subject: [PATCH 020/141] Added calls to set_name in register_signals so that the names of childred are set properly --- src/ophyd_async/tango/device_controllers/dgg2.py | 2 ++ src/ophyd_async/tango/device_controllers/omsvme58.py | 2 ++ src/ophyd_async/tango/device_controllers/sis3820.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/ophyd_async/tango/device_controllers/dgg2.py b/src/ophyd_async/tango/device_controllers/dgg2.py index f91ea0ccd8..6321d1c0f0 100644 --- a/src/ophyd_async/tango/device_controllers/dgg2.py +++ b/src/ophyd_async/tango/device_controllers/dgg2.py @@ -35,6 +35,8 @@ def register_signals(self): self.trl + "/startandwaitfortimer", device_proxy=self.proxy ) + self.set_name(self.name) + # -------------------------------------------------------------------- async def _trigger(self, watchers: List[Callable] = []): self._set_success = True diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index 8e27cf66ec..f757fd9383 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -51,6 +51,8 @@ def register_signals(self): self._stop = tango_signal_x(self.trl + "/stopmove", self.proxy) self._state = tango_signal_r(DevState, self.trl + "/state", self.proxy) + self.set_name(self.name) + # -------------------------------------------------------------------- async def _move(self, new_position: float, watchers: List[Callable] = []): self._set_success = True diff --git a/src/ophyd_async/tango/device_controllers/sis3820.py b/src/ophyd_async/tango/device_controllers/sis3820.py index 8700632560..845e23d2de 100644 --- a/src/ophyd_async/tango/device_controllers/sis3820.py +++ b/src/ophyd_async/tango/device_controllers/sis3820.py @@ -30,6 +30,8 @@ def register_signals(self): self.reset = tango_signal_x(self.trl + "/reset", device_proxy=self.proxy) + self.set_name(self.name) + # -------------------------------------------------------------------- # Theoretically counter has to be reset before triggering, but I do not how to do it # def trigger(self) -> AsyncStatus: From 93ed5aa62ac5b9227e0440019a7ae25059bea150 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 23 Apr 2024 15:36:06 +0200 Subject: [PATCH 021/141] Added polling of position to OmsVME58Motor every 0.5 seconds. --- src/ophyd_async/tango/device_controllers/omsvme58.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index f757fd9383..9ac055e534 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -76,8 +76,15 @@ def update_watchers(current_position: float): try: await self.position.set(new_position) await asyncio.sleep(0.1) + counter = 0 while await self._state.get_value() == DevState.MOVING: + # Update the watchers with the current position every 0.5 seconds + if counter % 5 == 0: + current_position = await self.position.get_value() + update_watchers(current_position) + counter = 0 await asyncio.sleep(0.1) + counter += 1 finally: if self.position.is_cachable(): self.position.clear_sub(update_watchers) From 7e87bc7a3f079f9b1973a92d5ff4c18c926f37ce Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 30 Apr 2024 15:02:16 +0200 Subject: [PATCH 022/141] Added source method to TangoSignal and _backend which has the @property decorator. Compliance with ruff linting --- .../tango/_backend/_signal_backend.py | 26 +++++-- .../tango/_backend/_tango_transport.py | 78 +++++++++++-------- .../tango/base_devices/base_device.py | 3 +- .../tango/device_controllers/omsvme58.py | 4 +- src/ophyd_async/tango/sardana/motor.py | 4 +- src/ophyd_async/tango/signal/signal.py | 7 +- tests/tango/test_base_device.py | 35 +++++---- tests/tango/test_ddg2.py | 4 +- tests/tango/test_motors.py | 7 +- tests/tango/test_scans.py | 5 +- tests/tango/test_sis3820.py | 4 +- tests/tango/test_tango_signals.py | 31 ++++---- 12 files changed, 111 insertions(+), 97 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_signal_backend.py b/src/ophyd_async/tango/_backend/_signal_backend.py index a1b816d21a..0db6d0d0af 100644 --- a/src/ophyd_async/tango/_backend/_signal_backend.py +++ b/src/ophyd_async/tango/_backend/_signal_backend.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional +from typing import Callable, Optional from ophyd_async.core import SignalR, SignalRW, SignalW, SignalX, T from ophyd_async.core.signal import add_timeout @@ -24,16 +24,32 @@ def is_cachable(self) -> T: # -------------------------------------------------------------------- -class TangoSignalW(SignalW[T], CachableOrNot, SignalWithSetpoint): ... # noqa: E701 +class TangoSignalW(SignalW[T], CachableOrNot, SignalWithSetpoint): + # -------------------------------------------------------------------- + @property + def source(self) -> Callable[[], str]: + return self._backend.source # -------------------------------------------------------------------- -class TangoSignalRW(SignalRW[T], CachableOrNot, SignalWithSetpoint): ... # noqa: E701 +class TangoSignalRW(SignalRW[T], CachableOrNot, SignalWithSetpoint): + # -------------------------------------------------------------------- + @property + def source(self) -> Callable[[], str]: + return self._backend.source # -------------------------------------------------------------------- -class TangoSignalR(SignalR[T], CachableOrNot): ... # noqa: E701 +class TangoSignalR(SignalR[T], CachableOrNot): + # -------------------------------------------------------------------- + @property + def source(self) -> Callable[[], str]: + return self._backend.source # -------------------------------------------------------------------- -class TangoSignalX(SignalX, CachableOrNot): ... # noqa: E701 +class TangoSignalX(SignalX, CachableOrNot): + # -------------------------------------------------------------------- + @property + def source(self) -> Callable[[], str]: + return self._backend.source diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index a1ff0d229e..8613ac7adc 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -8,6 +8,17 @@ import numpy as np from bluesky.protocols import Descriptor, Reading + +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + NotConnected, + ReadingValueCallback, + SignalBackend, + T, + get_dtype, + get_unique, + wait_for_connection, +) from tango import ( AttrDataFormat, AttributeInfoEx, @@ -24,17 +35,6 @@ ) from tango.utils import is_array, is_binary, is_bool, is_float, is_int, is_str -from ophyd_async.core import ( - DEFAULT_TIMEOUT, - NotConnected, - ReadingValueCallback, - SignalBackend, - T, - get_dtype, - get_unique, - wait_for_connection, -) - __all__ = ("TangoTransport", "TangoSignalBackend") @@ -94,7 +94,6 @@ def get_python_type(tango_type) -> tuple[bool, T, str]: # -------------------------------------------------------------------- class TangoProxy: - support_events = False def __init__(self, device_proxy: DeviceProxy, name: str): @@ -150,7 +149,6 @@ def unsubscribe_callback(self): # -------------------------------------------------------------------- class AttributeProxy(TangoProxy): - support_events = False _event_callback = None _eid = None @@ -205,11 +203,11 @@ async def get_config(self) -> AttributeInfoEx: @ensure_proper_executor async def get_reading(self) -> Reading: attr = await self._proxy.read_attribute(self._name) - return dict( - value=attr.value, - timestamp=attr.time.totime(), - alarm_severity=attr.quality, - ) + return { + "value": attr.value, + "timestamp": attr.time.totime(), + "alarm_severity": attr.quality, + } # -------------------------------------------------------------------- def has_subscription(self) -> bool: @@ -238,20 +236,19 @@ def unsubscribe_callback(self): def _event_processor(self, event): if not event.err: value = event.attr_value.value - reading = dict( - value=value, - timestamp=event.get_date().totime(), - alarm_severity=event.attr_value.quality, - ) + reading = { + "value": value, + "timestamp": event.get_date().totime(), + "alarm_severity": event.attr_value.quality, + } self._event_callback(reading, value) # -------------------------------------------------------------------- class CommandProxy(TangoProxy): - support_events = False - _last_reading = dict(value=None, timestamp=0, alarm_severity=0) + _last_reading = {"value": None, "timestamp": 0, "alarm_severity": 0} # -------------------------------------------------------------------- async def get(self) -> T: @@ -276,7 +273,11 @@ async def put( except Exception: await asyncio.sleep(A_BIT) - self._last_reading = dict(value=val, timestamp=time.time(), alarm_severity=0) + self._last_reading = { + "value": val, + "timestamp": time.time(), + "alarm_severity": 0, + } # -------------------------------------------------------------------- @ensure_proper_executor @@ -357,9 +358,9 @@ def get_trl_descriptor( raise TypeError(f"{tango_resource} has type [{tr_dtype}] not [{dtype}]") if tr_format == AttrDataFormat.SPECTRUM: - return dict(source=tango_resource, dtype="array", shape=[max_x]) + return {"source": tango_resource, "dtype": "array", "shape": [max_x]} elif tr_format == AttrDataFormat.IMAGE: - return dict(source=tango_resource, dtype="array", shape=[max_y, max_x]) + return {"source": tango_resource, "dtype": "array", "shape": [max_y, max_x]} else: if tr_dtype in (Enum, CmdArgType.DevState): @@ -377,16 +378,19 @@ def get_trl_descriptor( raise TypeError( f"{tango_resource} has choices {trl_choices} not {choices}" ) - return dict( - source=tango_resource, dtype="string", shape=[], choices=trl_choices - ) + return { + "source": tango_resource, + "dtype": "string", + "shape": [], + "choices": trl_choices, + } else: if datatype and not issubclass(tr_dtype, datatype): raise TypeError( f"{tango_resource} has type {tr_dtype.__name__} " f"not {datatype.__name__}" ) - return dict(source=tango_resource, dtype=tr_dtype_desc, shape=[]) + return {"source": tango_resource, "dtype": tr_dtype_desc, "shape": []} # -------------------------------------------------------------------- @@ -417,7 +421,6 @@ async def get_tango_trl( # -------------------------------------------------------------------- class TangoTransport(TangoSignalBackend[T]): - def __init__( self, datatype: Optional[Type[T]], @@ -433,9 +436,14 @@ def __init__( write_trl: device_proxy, } self.trl_configs: Dict[str, AttributeInfoEx] = {} - self.source = f"{self.read_trl}" + # self.source = f"{self.read_trl}" self.descriptor: Descriptor = {} # type: ignore + # -------------------------------------------------------------------- + @property + def source(self) -> str: + return f"{self.read_trl}" + # -------------------------------------------------------------------- async def _connect_and_store_config(self, trl): try: @@ -504,3 +512,5 @@ def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: else: self.proxies[self.read_trl].unsubscribe_callback() + + # -------------------------------------------------------------------- diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index 32a06414e7..f46a10d4db 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -4,9 +4,8 @@ from abc import abstractmethod -from tango.asyncio import DeviceProxy - from ophyd_async.core import AsyncStatus, StandardReadable +from tango.asyncio import DeviceProxy __all__ = ("TangoReadableDevice",) diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index 9ac055e534..8c51c0bd33 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -5,7 +5,6 @@ from typing import Callable, List, Optional from bluesky.protocols import Locatable, Location, Stoppable -from tango import DevState from ophyd_async.core import AsyncStatus from ophyd_async.tango import ( @@ -14,11 +13,11 @@ tango_signal_rw, tango_signal_x, ) +from tango import DevState # -------------------------------------------------------------------- class OmsVME58Motor(TangoReadableDevice, Locatable, Stoppable): - # -------------------------------------------------------------------- def __init__(self, trl: str, name="") -> None: TangoReadableDevice.__init__(self, trl, name) @@ -26,7 +25,6 @@ def __init__(self, trl: str, name="") -> None: # -------------------------------------------------------------------- def register_signals(self): - self.position = tango_signal_rw( float, self.trl + "/position", device_proxy=self.proxy ) diff --git a/src/ophyd_async/tango/sardana/motor.py b/src/ophyd_async/tango/sardana/motor.py index 15e87ea603..520d49ebba 100644 --- a/src/ophyd_async/tango/sardana/motor.py +++ b/src/ophyd_async/tango/sardana/motor.py @@ -5,7 +5,6 @@ from typing import Callable, List, Optional from bluesky.protocols import Locatable, Location, Stoppable -from tango import DevState from ophyd_async.core import AsyncStatus from ophyd_async.tango import ( @@ -14,11 +13,11 @@ tango_signal_rw, tango_signal_x, ) +from tango import DevState # -------------------------------------------------------------------- class SardanaMotor(TangoReadableDevice, Locatable, Stoppable): - # -------------------------------------------------------------------- def __init__(self, trl: str, name="") -> None: TangoReadableDevice.__init__(self, trl, name) @@ -26,7 +25,6 @@ def __init__(self, trl: str, name="") -> None: # -------------------------------------------------------------------- def register_signals(self): - self.position = tango_signal_rw( float, self.trl + "/Position", device_proxy=self.proxy ) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 3d36d3c87b..2f815eca6e 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -4,10 +4,6 @@ from typing import Optional, Type, Union -from tango import AttrWriteType, CmdArgType -from tango import DeviceProxy as SyncDeviceProxy -from tango.asyncio import DeviceProxy - from ophyd_async.core import T from ophyd_async.tango._backend import ( TangoSignalBackend, @@ -17,6 +13,9 @@ TangoSignalX, TangoTransport, ) +from tango import AttrWriteType, CmdArgType +from tango import DeviceProxy as SyncDeviceProxy +from tango.asyncio import DeviceProxy __all__ = ( "tango_signal_rw", diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index e88a0b72a4..ab3b791248 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -6,6 +6,10 @@ import pytest from bluesky import RunEngine from bluesky.plans import count + +from ophyd_async.core import DeviceCollector, T +from ophyd_async.tango import TangoReadableDevice, tango_signal_auto +from ophyd_async.tango._backend._tango_transport import get_python_type from tango import ( AttrDataFormat, AttrQuality, @@ -19,10 +23,6 @@ from tango.test_context import MultiDeviceTestContext from tango.test_utils import assert_close -from ophyd_async.core import DeviceCollector, T -from ophyd_async.tango import TangoReadableDevice, tango_signal_auto -from ophyd_async.tango._backend._tango_transport import get_python_type - class TestEnum(IntEnum): __test__ = False @@ -151,23 +151,24 @@ def describe_class(fqtrl): # -------------------------------------------------------------------- def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: if python_type in [bool, int]: - return dict(dtype="integer", shape=[]) + return {"dtype": "integer", "shape": []} if python_type in [float]: - return dict(dtype="number", shape=[]) + return {"dtype": "number", "shape": []} if python_type in [str]: - return dict(dtype="string", shape=[]) + return {"dtype": "string", "shape": []} if issubclass(python_type, DevState): - return dict(dtype="string", shape=[], choices=list(DevState.names.keys())) + return {"dtype": "string", "shape": [], "choices": list(DevState.names.keys())} if issubclass(python_type, Enum): - return dict( - dtype="string", - shape=[], - choices=[] if is_cmd else [member.name for member in value.__class__], - ) + return { + "dtype": "string", + "shape": [], + "choices": [] if is_cmd else [member.name for member in python_type], + } - return dict( - dtype="array", shape=[np.Inf] if is_cmd else list(np.array(value).shape) - ) + return { + "dtype": "array", + "shape": [np.Inf] if is_cmd else list(np.array(value).shape), + } # -------------------------------------------------------------------- @@ -187,7 +188,7 @@ def reset_tango_asyncio(): # -------------------------------------------------------------------- def compare_values(expected, received): - assert set(list(expected.keys())) == set(list(received.keys())) + assert set(expected.keys()) == set(received.keys()) for k, v in expected.items(): for _k, _v in v.items(): assert_close(_v, received[k][_k]) diff --git a/tests/tango/test_ddg2.py b/tests/tango/test_ddg2.py index a192ac16fd..b0f0c7aae7 100644 --- a/tests/tango/test_ddg2.py +++ b/tests/tango/test_ddg2.py @@ -3,10 +3,10 @@ import pytest from bluesky import Msg, RunEngine from bluesky.plans import count -from tango.asyncio_executor import set_global_executor from ophyd_async.core import DeviceCollector from ophyd_async.tango.device_controllers import DGG2Timer +from tango.asyncio_executor import set_global_executor # -------------------------------------------------------------------- @@ -27,14 +27,12 @@ async def dgg2(): # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_connect(dgg2): - assert dgg2.name == "dgg2" # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_readout_with_bluesky(dgg2): - TEST_TIME = 0.1 await dgg2.set_time(TEST_TIME) diff --git a/tests/tango/test_motors.py b/tests/tango/test_motors.py index ef0e027a8a..85355f86d0 100644 --- a/tests/tango/test_motors.py +++ b/tests/tango/test_motors.py @@ -6,11 +6,11 @@ from bluesky import RunEngine from bluesky.plan_stubs import mv from bluesky.plans import count, scan -from tango.asyncio_executor import set_global_executor from ophyd_async.core import DeviceCollector from ophyd_async.tango.device_controllers import OmsVME58Motor from ophyd_async.tango.sardana import SardanaMotor +from tango.asyncio_executor import set_global_executor # Long enough for multiple asyncio event loop cycles to run so # all the tasks have a chance to run @@ -50,14 +50,12 @@ async def dummy_motor(motor_to_test): # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_connect(dummy_motor): - assert dummy_motor.name == "dummy_motor" # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_readout_with_bluesky(dummy_motor): - # now let's do some bluesky stuff RE = RunEngine() RE(count([dummy_motor], 1)) @@ -106,7 +104,6 @@ async def test_move_with_bluesky(dummy_motor): # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_scan_motor_vs_motor_position(dummy_motor): - readouts = Mock() # now let's do some bluesky stuff @@ -114,7 +111,7 @@ async def test_scan_motor_vs_motor_position(dummy_motor): RE(scan([dummy_motor.position], dummy_motor, 0, 1, num=11), readouts) assert readouts.call_count == 14 - assert set([args[0][0] for args in readouts.call_args_list]) == { + assert {args[0][0] for args in readouts.call_args_list} == { "descriptor", "event", "start", diff --git a/tests/tango/test_scans.py b/tests/tango/test_scans.py index 0109c392a5..211d6cb1df 100644 --- a/tests/tango/test_scans.py +++ b/tests/tango/test_scans.py @@ -4,7 +4,6 @@ import pytest from bluesky import RunEngine from bluesky.plans import scan -from tango.asyncio_executor import set_global_executor from ophyd_async.core import DeviceCollector from ophyd_async.tango.device_controllers import ( @@ -12,6 +11,7 @@ OmsVME58Motor, SIS3820Counter, ) +from tango.asyncio_executor import set_global_executor # Long enough for multiple asyncio event loop cycles to run so # all the tasks have a chance to run @@ -38,7 +38,6 @@ async def devices_set(): # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_step_scan_motor_vs_counter_with_dgg2(devices_set): - omsvme58_motor, dgg2timer, sis3820 = devices_set readouts = Mock() @@ -51,7 +50,7 @@ async def test_step_scan_motor_vs_counter_with_dgg2(devices_set): ) assert readouts.call_count == 14 - assert set([args[0][0] for args in readouts.call_args_list]) == { + assert {arg[0][0] for arg in readouts.call_args_list} == { "descriptor", "event", "start", diff --git a/tests/tango/test_sis3820.py b/tests/tango/test_sis3820.py index b01c787b87..e7ca2d2169 100644 --- a/tests/tango/test_sis3820.py +++ b/tests/tango/test_sis3820.py @@ -3,10 +3,10 @@ import pytest from bluesky import RunEngine from bluesky.plans import count -from tango.asyncio_executor import set_global_executor from ophyd_async.core import DeviceCollector from ophyd_async.tango.device_controllers import SIS3820Counter +from tango.asyncio_executor import set_global_executor # -------------------------------------------------------------------- @@ -27,14 +27,12 @@ async def sis3820(): # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_connect(sis3820): - assert sis3820.name == "sis3820" # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_readout_with_bluesky(sis3820): - readouts = Mock() # now let's do some bluesky stuff diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index 7922e3ac5f..1878178541 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -9,15 +9,15 @@ import numpy.typing as npt import pytest from bluesky.protocols import Reading + +from ophyd_async.core import SignalBackend, T +from ophyd_async.tango._backend import TangoSignalBackend, TangoTransport from tango import AttrDataFormat, AttrWriteType, DeviceProxy, DevState from tango.asyncio_executor import set_global_executor from tango.server import Device, attribute, command from tango.test_context import MultiDeviceTestContext from tango.test_utils import assert_close -from ophyd_async.core import SignalBackend, T -from ophyd_async.tango._backend import TangoSignalBackend, TangoTransport - # -------------------------------------------------------------------- """ Since TangoTest does not support EchoMode, we create our own Device. @@ -197,23 +197,24 @@ def reset_tango_asyncio(): # -------------------------------------------------------------------- def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: if python_type in [bool, int]: - return dict(dtype="integer", shape=[]) + return {"dtype": "integer", "shape": []} if python_type in [float]: - return dict(dtype="number", shape=[]) + return {"dtype": "number", "shape": []} if python_type in [str]: - return dict(dtype="string", shape=[]) + return {"dtype": "string", "shape": []} if issubclass(python_type, DevState): - return dict(dtype="string", shape=[], choices=list(DevState.names.keys())) + return {"dtype": "string", "shape": [], "choices": list(DevState.names.keys())} if issubclass(python_type, Enum): - return dict( - dtype="string", - shape=[], - choices=[] if is_cmd else [member.name for member in value.__class__], - ) + return { + "dtype": "string", + "shape": [], + "choices": [] if is_cmd else [member.name for member in value.__class__], + } - return dict( - dtype="array", shape=[np.Inf] if is_cmd else list(np.array(value).shape) - ) + return { + "dtype": "array", + "shape": [np.Inf] if is_cmd else list(np.array(value).shape), + } # -------------------------------------------------------------------- From b3256844a1435821e48c7deddd44ed2b17460f64 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 30 Apr 2024 16:22:22 +0200 Subject: [PATCH 023/141] DeviceProxy is now being awaited in connect so that Devices do not have to be awaited from the startup script. This appears to only be working for the OmsVME58Motor object as of writing. Timers and Counters are not connecting properly. --- .../tango/base_devices/base_device.py | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index f46a10d4db..b59adced05 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -1,10 +1,8 @@ -"""Default Tango Devices""" - from __future__ import annotations from abc import abstractmethod -from ophyd_async.core import AsyncStatus, StandardReadable +from ophyd_async.core import DEFAULT_TIMEOUT, AsyncStatus, StandardReadable from tango.asyncio import DeviceProxy __all__ = ("TangoReadableDevice",) @@ -26,15 +24,21 @@ def __init__(self, trl: str, name="") -> None: self.proxy: DeviceProxy = None StandardReadable.__init__(self, name=name) - # -------------------------------------------------------------------- - def __await__(self): - async def closure(): - self.proxy = await DeviceProxy(self.trl) - self.register_signals() - - return self + # # -------------------------------------------------------------------- + # def __await__(self): + # async def closure(): + # self.proxy = await DeviceProxy(self.trl) + # self.register_signals() + # + # return self + # + # return closure().__await__() - return closure().__await__() + # -------------------------------------------------------------------- + async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + self.proxy = await DeviceProxy(self.trl) + self.register_signals() + await super().connect(sim=sim, timeout=timeout) # -------------------------------------------------------------------- @abstractmethod From 906409c8a4dc62aebe7f1a66bfab2bd769243602 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 2 May 2024 13:32:57 +0200 Subject: [PATCH 024/141] changed _read_signals and _config_signals to _readables and _configurables to comply with changes to StandardReadable --- src/ophyd_async/tango/base_devices/base_device.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index b59adced05..d96cdabcd6 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -36,8 +36,12 @@ def __init__(self, trl: str, name="") -> None: # -------------------------------------------------------------------- async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): - self.proxy = await DeviceProxy(self.trl) - self.register_signals() + async def closure(): + self.proxy = await DeviceProxy(self.trl) + self.register_signals() + return self + + await closure() await super().connect(sim=sim, timeout=timeout) # -------------------------------------------------------------------- @@ -50,13 +54,13 @@ def register_signals(self): # -------------------------------------------------------------------- @AsyncStatus.wrap async def stage(self) -> None: - for sig in self._read_signals + self._configuration_signals: + for sig in self._readables + self._configurables: if hasattr(sig, "is_cachable") and sig.is_cachable(): await sig.stage().task # -------------------------------------------------------------------- @AsyncStatus.wrap async def unstage(self) -> None: - for sig in self._read_signals + self._configuration_signals: + for sig in self._readables + self._configurables: if hasattr(sig, "is_cachable") and sig.is_cachable(): await sig.unstage().task From 0df5b0ff09ecf6a3ed14c405d5c005f8c36be24c Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 2 May 2024 13:36:48 +0200 Subject: [PATCH 025/141] Made get_descriptor signature compliant with changes to SignalBacken. --- src/ophyd_async/tango/_backend/_tango_transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 8613ac7adc..e0f3a5fb54 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -473,7 +473,7 @@ async def put(self, write_value: Optional[T], wait=True, timeout=None): await self.proxies[self.write_trl].put(write_value, wait, timeout) # -------------------------------------------------------------------- - async def get_descriptor(self) -> Descriptor: + async def get_descriptor(self, source: str) -> Descriptor: return self.descriptor # -------------------------------------------------------------------- From 9889272b75be3a76d423a945380f3ec104f22ef2 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 3 May 2024 15:23:53 +0200 Subject: [PATCH 026/141] Created new 'TangoMover' class. A generic locatable and stoppable device. By passing a dictionary of signals upon initializing, it will read the dictionary to add the appropriate signals. position, _stop and _state signals are required. --- .../tango/device_controllers/__init__.py | 3 +- .../tango/device_controllers/tango_mover.py | 230 ++++++++++++++++++ 2 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 src/ophyd_async/tango/device_controllers/tango_mover.py diff --git a/src/ophyd_async/tango/device_controllers/__init__.py b/src/ophyd_async/tango/device_controllers/__init__.py index 59cf2d18f6..5812e13ebf 100644 --- a/src/ophyd_async/tango/device_controllers/__init__.py +++ b/src/ophyd_async/tango/device_controllers/__init__.py @@ -1,5 +1,6 @@ from ophyd_async.tango.device_controllers.dgg2 import DGG2Timer from ophyd_async.tango.device_controllers.omsvme58 import OmsVME58Motor from ophyd_async.tango.device_controllers.sis3820 import SIS3820Counter +from ophyd_async.tango.device_controllers.tango_mover import TangoMover -__all__ = ("OmsVME58Motor", "SIS3820Counter", "DGG2Timer") +__all__ = ("OmsVME58Motor", "SIS3820Counter", "DGG2Timer", "TangoMover") diff --git a/src/ophyd_async/tango/device_controllers/tango_mover.py b/src/ophyd_async/tango/device_controllers/tango_mover.py new file mode 100644 index 0000000000..cf2fbe76f5 --- /dev/null +++ b/src/ophyd_async/tango/device_controllers/tango_mover.py @@ -0,0 +1,230 @@ +from __future__ import annotations + +import asyncio +import time +from typing import Callable, List, Optional + +from bluesky.protocols import Locatable, Location, Stoppable + +from ophyd_async.core import AsyncStatus +from ophyd_async.tango import ( + TangoReadableDevice, + tango_signal_r, + tango_signal_rw, + tango_signal_w, + tango_signal_x, +) +from tango import DevState + + +# -------------------------------------------------------------------- +class TangoMover(TangoReadableDevice, Locatable, Stoppable): + # -------------------------------------------------------------------- + def __init__(self, trl: str, name="", signals: dict = None) -> None: + """ + Generic moveable tango device. Can be used in combination with an appropriately + formatted signal dictionary to instantiate any tango device that can be driven + along a single axis. + + signals must be a dictionary that contains at least the following keys. Each key + will be a signal of the same name on the device. + - position + - _state + - _stop + + Values must be dictionaries of the following form + { + "dtype": "float", + "source": "/position", + "sig_type": "rw" + "readable": True + "configurable": True + "read_uncached": False + } + + Parameters + ---------- + trl : str + The Tango device trl + name : str + The name of the device + signals : dict + A dictionary containing the signals for the device + """ + # Check that the signals dictionary is valid + if not signals: + raise ValueError("Signals dictionary must be provided") + if "position" not in signals: + raise ValueError("Signals dictionary must contain a position signal") + if "_state" not in signals: + raise ValueError("Signals dictionary must contain a _state signal") + if "_stop" not in signals: + raise ValueError("Signals dictionary must contain a _stop signal") + + self.position = None + self._state = None + self._stop = None + self._signal_list = signals + TangoReadableDevice.__init__(self, trl, name) + self._set_success = True + + # -------------------------------------------------------------------- + def _add_signal(self, **kwargs: str) -> None: + """ + Add a signal to the device + + Required Parameters + ---------- + dtype : The data type of the value of the signal + name : The name given to the signal object + source : The source of the signal, appended to the device trl + sig_type : The type of signal (r, w, rw, or x) + """ + required_keys = ["dtype", "name", "source", "sig_type"] + for key in required_keys: + if key not in kwargs: + raise ValueError(f"Missing required key {key}") + dtype = kwargs["dtype"] + name = kwargs["name"] + source = kwargs["source"] + sig_type = kwargs["sig_type"] + + # Convert the data type string to the actual type + if dtype.lower() == "int": + f_dtype = int + elif dtype.lower() == "float": + f_dtype = float + elif dtype.lower() == "str": + f_dtype = str + elif dtype == "DevState": + f_dtype = DevState + elif dtype.lower() == "none": + f_dtype = None + else: + raise ValueError(f"Invalid data type {dtype}") + + if sig_type == "r": + setattr( + self, + name, + tango_signal_r(f_dtype, self.trl + source, device_proxy=self.proxy), + ) + elif sig_type == "w": + setattr( + self, + name, + tango_signal_w(f_dtype, self.trl + source, device_proxy=self.proxy), + ) + elif sig_type == "rw": + setattr( + self, + name, + tango_signal_rw(f_dtype, self.trl + source, device_proxy=self.proxy), + ) + elif sig_type == "x": + setattr( + self, name, tango_signal_x(self.trl + source, device_proxy=self.proxy) + ) + else: + raise ValueError(f"Invalid signal type {sig_type}") + + # -------------------------------------------------------------------- + def register_signals(self) -> None: + # If signal names are not prepended by /, add it + for name in self._signal_list: + if not self._signal_list[name]["source"].startswith("/"): + self._signal_list[name]["source"] = ( + "/" + self._signal_list[name]["source"] + ) + + # If the dtype of the _state signal is not DevState, change it + if self._signal_list["_state"]["dtype"] != "DevState": + self._signal_list["_state"]["dtype"] = "DevState" + + # Parse the signal list and add the signals to the device + for name, signal in self._signal_list.items(): + self._add_signal(name=name, **signal) + + # Set the readable signals + self.set_readable_signals( + read=[ + getattr(self, name) + for name in self._signal_list + if self._signal_list[name]["readable"] + ], + config=[ + getattr(self, name) + for name in self._signal_list + if self._signal_list[name]["configurable"] + ], + read_uncached=[ + getattr(self, name) + for name in self._signal_list + if self._signal_list[name]["read_uncached"] + ], + ) + + self.set_name(self.name) + + # -------------------------------------------------------------------- + async def _move( + self, new_position: float, watchers: List[Callable] or None = None + ) -> None: + if watchers is None: + watchers = [] + self._set_success = True + start = time.monotonic() + start_position = await self.position.get_value() + + def update_watchers(current_position: float): + for watcher in watchers: + watcher( + name=self.name, + current=current_position, + initial=start_position, + target=new_position, + time_elapsed=time.monotonic() - start, + ) + + if self.position.is_cachable(): + self.position.subscribe_value(update_watchers) + else: + update_watchers(start_position) + try: + await self.position.set(new_position) + await asyncio.sleep(0.1) + counter = 0 + while await self._state.get_value() == DevState.MOVING: + # Update the watchers with the current position every 0.5 seconds + if counter % 5 == 0: + current_position = await self.position.get_value() + update_watchers(current_position) + counter = 0 + await asyncio.sleep(0.1) + counter += 1 + finally: + if self.position.is_cachable(): + self.position.clear_sub(update_watchers) + else: + update_watchers(await self.position.get_value()) + if not self._set_success: + raise RuntimeError("Motor was stopped") + + # -------------------------------------------------------------------- + def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus: + watchers: List[Callable] = [] + coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout) + return AsyncStatus(coro, watchers) + + # -------------------------------------------------------------------- + async def locate(self) -> Location: + set_point = await self.position.get_setpoint() + readback = await self.position.get_value() + return Location(setpoint=set_point, readback=readback) + + # -------------------------------------------------------------------- + async def stop(self, success: bool = True) -> None: + self._set_success = not success + # Put with completion will never complete as we are waiting for completion on + # the move above, so need to pass wait=False + await self._stop.trigger() From ecc6111c7e75ae68afe571a5878a6033baf14941 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 23 May 2024 16:23:46 +0200 Subject: [PATCH 027/141] Tuning sample device definitions --- .../tango/device_controllers/dgg2.py | 31 ++++++++++++++++--- .../tango/device_controllers/omsvme58.py | 4 ++- .../tango/device_controllers/sis3820.py | 7 +++-- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/ophyd_async/tango/device_controllers/dgg2.py b/src/ophyd_async/tango/device_controllers/dgg2.py index 6321d1c0f0..8de6ce5b56 100644 --- a/src/ophyd_async/tango/device_controllers/dgg2.py +++ b/src/ophyd_async/tango/device_controllers/dgg2.py @@ -1,17 +1,23 @@ from __future__ import annotations +import asyncio import time from typing import Callable, List from bluesky.protocols import Preparable, Triggerable from ophyd_async.core import AsyncStatus -from ophyd_async.tango import TangoReadableDevice, tango_signal_rw, tango_signal_x +from ophyd_async.tango import ( + TangoReadableDevice, + tango_signal_r, + tango_signal_rw, + tango_signal_x, +) +from tango import DevState # -------------------------------------------------------------------- class DGG2Timer(TangoReadableDevice, Triggerable, Preparable): - # -------------------------------------------------------------------- def __init__(self, trl: str, name="") -> None: TangoReadableDevice.__init__(self, trl, name) @@ -19,7 +25,6 @@ def __init__(self, trl: str, name="") -> None: # -------------------------------------------------------------------- def register_signals(self): - self.sampletime = tango_signal_rw( float, self.trl + "/sampletime", device_proxy=self.proxy ) @@ -35,6 +40,10 @@ def register_signals(self): self.trl + "/startandwaitfortimer", device_proxy=self.proxy ) + self.start = tango_signal_x(self.trl + "/start", device_proxy=self.proxy) + + self._state = tango_signal_r(DevState, self.trl + "/state", self.proxy) + self.set_name(self.name) # -------------------------------------------------------------------- @@ -58,12 +67,24 @@ def update_watchers(remaining_time: float): else: update_watchers(total_time) try: - await self.startandwaitfortimer.trigger() + await self.start.trigger() finally: if self.remainingtime.is_cachable(): self.remainingtime.clear_sub(update_watchers) else: - update_watchers(await self.remainingtime.get_value()) + counter = 0 + state = await self._state.get_value() + while state == DevState.MOVING: + # Update the watchers with the current position every 0.5 seconds + if counter % 5 == 0: + remaining_time = await self.remainingtime.get_value() + update_watchers(remaining_time) + counter = 0 + await asyncio.sleep(0.1) + state = await self._state.get_value() + counter += 1 + + # update_watchers(await self.remainingtime.get_value()) if not self._set_success: raise RuntimeError("Timer was not triggered") diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index 8c51c0bd33..0f62f4a400 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -75,13 +75,15 @@ def update_watchers(current_position: float): await self.position.set(new_position) await asyncio.sleep(0.1) counter = 0 - while await self._state.get_value() == DevState.MOVING: + state = await self._state.get_value() + while state == DevState.MOVING: # Update the watchers with the current position every 0.5 seconds if counter % 5 == 0: current_position = await self.position.get_value() update_watchers(current_position) counter = 0 await asyncio.sleep(0.1) + state = await self._state.get_value() counter += 1 finally: if self.position.is_cachable(): diff --git a/src/ophyd_async/tango/device_controllers/sis3820.py b/src/ophyd_async/tango/device_controllers/sis3820.py index 845e23d2de..fd56a62fe8 100644 --- a/src/ophyd_async/tango/device_controllers/sis3820.py +++ b/src/ophyd_async/tango/device_controllers/sis3820.py @@ -10,7 +10,6 @@ # -------------------------------------------------------------------- class SIS3820Counter(TangoReadableDevice): # Triggerable - # -------------------------------------------------------------------- def __init__(self, trl: str, name="") -> None: TangoReadableDevice.__init__(self, trl, name) @@ -18,7 +17,6 @@ def __init__(self, trl: str, name="") -> None: # -------------------------------------------------------------------- def register_signals(self): - self.counts = tango_signal_rw( float, self.trl + "/counts", device_proxy=self.proxy ) @@ -40,5 +38,8 @@ def register_signals(self): # -------------------------------------------------------------------- async def read(self) -> Dict[str, Reading]: ret = await super().read() - await self.reset.trigger() return ret + + async def stage(self): + await self.reset.trigger() + await super().stage() From 6729c432650eeb7f032ebe09b2dcdad0e0eccbcb Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 24 May 2024 16:27:09 +0200 Subject: [PATCH 028/141] Added some typing. Fixed stop signature for OmsVMEMotor --- .../tango/device_controllers/dgg2.py | 10 ++++++---- .../tango/device_controllers/omsvme58.py | 18 ++++++++++-------- .../tango/device_controllers/sis3820.py | 17 +++++++---------- .../tango/device_controllers/tango_mover.py | 2 +- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/ophyd_async/tango/device_controllers/dgg2.py b/src/ophyd_async/tango/device_controllers/dgg2.py index 8de6ce5b56..e8316774ab 100644 --- a/src/ophyd_async/tango/device_controllers/dgg2.py +++ b/src/ophyd_async/tango/device_controllers/dgg2.py @@ -24,7 +24,7 @@ def __init__(self, trl: str, name="") -> None: self._set_success = True # -------------------------------------------------------------------- - def register_signals(self): + def register_signals(self) -> None: self.sampletime = tango_signal_rw( float, self.trl + "/sampletime", device_proxy=self.proxy ) @@ -47,12 +47,14 @@ def register_signals(self): self.set_name(self.name) # -------------------------------------------------------------------- - async def _trigger(self, watchers: List[Callable] = []): + async def _trigger(self, watchers: List[Callable] or None = None) -> None: + if watchers is None: + watchers = [] self._set_success = True start = time.monotonic() total_time = await self.sampletime.get_value() - def update_watchers(remaining_time: float): + def update_watchers(remaining_time: float) -> None: for watcher in watchers: watcher( name=self.name, @@ -94,7 +96,7 @@ def trigger(self) -> AsyncStatus: return AsyncStatus(self._trigger(watchers), watchers) # -------------------------------------------------------------------- - def prepare(self, p_time) -> AsyncStatus: + def prepare(self, p_time: float) -> AsyncStatus: return self.sampletime.set(p_time) # -------------------------------------------------------------------- diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index 0f62f4a400..cef6b819d1 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -19,12 +19,12 @@ # -------------------------------------------------------------------- class OmsVME58Motor(TangoReadableDevice, Locatable, Stoppable): # -------------------------------------------------------------------- - def __init__(self, trl: str, name="") -> None: + def __init__(self, trl: str, name: str = "") -> None: TangoReadableDevice.__init__(self, trl, name) self._set_success = True # -------------------------------------------------------------------- - def register_signals(self): + def register_signals(self) -> None: self.position = tango_signal_rw( float, self.trl + "/position", device_proxy=self.proxy ) @@ -52,12 +52,16 @@ def register_signals(self): self.set_name(self.name) # -------------------------------------------------------------------- - async def _move(self, new_position: float, watchers: List[Callable] = []): + async def _move( + self, new_position: float, watchers: List[Callable] or None = None + ) -> None: + if watchers is None: + watchers = [] self._set_success = True start = time.monotonic() start_position = await self.position.get_value() - def update_watchers(current_position: float): + def update_watchers(current_position: float) -> None: for watcher in watchers: watcher( name=self.name, @@ -106,8 +110,6 @@ async def locate(self) -> Location: return Location(setpoint=set_point, readback=readback) # -------------------------------------------------------------------- - async def stop(self, success=False): + def stop(self, success: bool = False) -> AsyncStatus: self._set_success = success - # Put with completion will never complete as we are waiting for completion on - # the move above, so need to pass wait=False - await self._stop.trigger() + return AsyncStatus(self._stop.trigger()) diff --git a/src/ophyd_async/tango/device_controllers/sis3820.py b/src/ophyd_async/tango/device_controllers/sis3820.py index fd56a62fe8..4df4b9c8e8 100644 --- a/src/ophyd_async/tango/device_controllers/sis3820.py +++ b/src/ophyd_async/tango/device_controllers/sis3820.py @@ -5,18 +5,19 @@ # from bluesky.protocols import Triggerable from bluesky.protocols import Reading +from ophyd_async.core import AsyncStatus from ophyd_async.tango import TangoReadableDevice, tango_signal_rw, tango_signal_x # -------------------------------------------------------------------- class SIS3820Counter(TangoReadableDevice): # Triggerable # -------------------------------------------------------------------- - def __init__(self, trl: str, name="") -> None: + def __init__(self, trl: str, name: str = "") -> None: TangoReadableDevice.__init__(self, trl, name) self._set_success = True # -------------------------------------------------------------------- - def register_signals(self): + def register_signals(self) -> None: self.counts = tango_signal_rw( float, self.trl + "/counts", device_proxy=self.proxy ) @@ -30,16 +31,12 @@ def register_signals(self): self.set_name(self.name) - # -------------------------------------------------------------------- - # Theoretically counter has to be reset before triggering, but I do not how to do it - # def trigger(self) -> AsyncStatus: - # return self.reset.trigger() - - # -------------------------------------------------------------------- async def read(self) -> Dict[str, Reading]: ret = await super().read() return ret - async def stage(self): + def trigger(self) -> AsyncStatus: + return AsyncStatus(self._trigger()) + + async def _trigger(self) -> None: await self.reset.trigger() - await super().stage() diff --git a/src/ophyd_async/tango/device_controllers/tango_mover.py b/src/ophyd_async/tango/device_controllers/tango_mover.py index cf2fbe76f5..6525d4995c 100644 --- a/src/ophyd_async/tango/device_controllers/tango_mover.py +++ b/src/ophyd_async/tango/device_controllers/tango_mover.py @@ -176,7 +176,7 @@ async def _move( start = time.monotonic() start_position = await self.position.get_value() - def update_watchers(current_position: float): + def update_watchers(current_position: float) -> None: for watcher in watchers: watcher( name=self.name, From a084558776265d76ccbafdcabd651f7bc55bf886 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 28 May 2024 10:49:01 +0200 Subject: [PATCH 029/141] Example devices can now take an options sources keyword. This is a dict that maps the expected signal names to a tango source attribute. If the sources are not the default names expected by the class, these can be modified here. --- .../tango/_backend/_tango_transport.py | 10 +++-- .../tango/device_controllers/dgg2.py | 40 ++++++++++++++--- .../tango/device_controllers/omsvme58.py | 44 +++++++++++++++---- .../tango/device_controllers/sis3820.py | 28 ++++++++++-- 4 files changed, 100 insertions(+), 22 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index e0f3a5fb54..93cce421b4 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -411,10 +411,12 @@ async def get_tango_trl( if trl_name in all_cmds: return CommandProxy(device_proxy, trl_name) - # all pipes can be always accessible with low register - all_pipes = [pipe_name.lower() for pipe_name in device_proxy.get_pipe_list()] - if trl_name in all_pipes: - raise NotImplementedError("Pipes are not supported") + # If version is below tango 9, then pipes are not supported + if device_proxy.info().server_version >= 9: + # all pipes can be always accessible with low register + all_pipes = [pipe_name.lower() for pipe_name in device_proxy.get_pipe_list()] + if trl_name in all_pipes: + raise NotImplementedError("Pipes are not supported") raise RuntimeError(f"{trl_name} cannot be found in {device_proxy.name()}") diff --git a/src/ophyd_async/tango/device_controllers/dgg2.py b/src/ophyd_async/tango/device_controllers/dgg2.py index e8316774ab..c8cdfbc2f9 100644 --- a/src/ophyd_async/tango/device_controllers/dgg2.py +++ b/src/ophyd_async/tango/device_controllers/dgg2.py @@ -18,18 +18,42 @@ # -------------------------------------------------------------------- class DGG2Timer(TangoReadableDevice, Triggerable, Preparable): + trl: str + name: str + src_dict: dict = { + "sampletime": "/sampletime", + "remainingtime": "/remainingtime", + "startandwaitfortimer": "/startandwaitfortimer", + "start": "/start", + "state": "/state", + } + # -------------------------------------------------------------------- - def __init__(self, trl: str, name="") -> None: + def __init__(self, trl: str, name="", sources: dict = None) -> None: + if sources is None: + sources = {} + self.src_dict["sampletime"] = sources.get("sampletime", "/sampletime") + self.src_dict["remainingtime"] = sources.get("remainingtime", "/remainingtime") + self.src_dict["startandwaitfortimer"] = sources.get( + "startandwaitfortimer", "/startandwaitfortimer" + ) + self.src_dict["start"] = sources.get("start", "/start") + self.src_dict["state"] = sources.get("state", "/state") + + for key in self.src_dict: + if not self.src_dict[key].startswith("/"): + self.src_dict[key] = "/" + self.src_dict[key] + TangoReadableDevice.__init__(self, trl, name) self._set_success = True # -------------------------------------------------------------------- def register_signals(self) -> None: self.sampletime = tango_signal_rw( - float, self.trl + "/sampletime", device_proxy=self.proxy + float, self.trl + self.src_dict["sampletime"], device_proxy=self.proxy ) self.remainingtime = tango_signal_rw( - float, self.trl + "/remainingtime", device_proxy=self.proxy + float, self.trl + self.src_dict["remainingtime"], device_proxy=self.proxy ) self.set_readable_signals( @@ -37,12 +61,16 @@ def register_signals(self) -> None: ) self.startandwaitfortimer = tango_signal_x( - self.trl + "/startandwaitfortimer", device_proxy=self.proxy + self.trl + self.src_dict["startandwaitfortimer"], device_proxy=self.proxy ) - self.start = tango_signal_x(self.trl + "/start", device_proxy=self.proxy) + self.start = tango_signal_x( + self.trl + self.src_dict["start"], device_proxy=self.proxy + ) - self._state = tango_signal_r(DevState, self.trl + "/state", self.proxy) + self._state = tango_signal_r( + DevState, self.trl + self.src_dict["state"], self.proxy + ) self.set_name(self.name) diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index cef6b819d1..09d06c6109 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -18,27 +18,53 @@ # -------------------------------------------------------------------- class OmsVME58Motor(TangoReadableDevice, Locatable, Stoppable): + trl: str + name: str + src_dict: dict = { + "position": "/position", + "baserate": "/baserate", + "slewrate": "/slewrate", + "conversion": "/conversion", + "acceleration": "/acceleration", + "stop": "/stopmove", + "state": "/state", + } + # -------------------------------------------------------------------- - def __init__(self, trl: str, name: str = "") -> None: + def __init__(self, trl: str, name: str = "", sources: dict = None) -> None: + if sources is None: + sources = {} + self.src_dict["position"] = sources.get("position", "/position") + self.src_dict["baserate"] = sources.get("baserate", "/baserate") + self.src_dict["slewrate"] = sources.get("slewrate", "/slewrate") + self.src_dict["conversion"] = sources.get("conversion", "/conversion") + self.src_dict["acceleration"] = sources.get("acceleration", "/acceleration") + self.src_dict["stop"] = sources.get("stop", "/stopmove") + self.src_dict["state"] = sources.get("state", "/state") + + for key in self.src_dict: + if not self.src_dict[key].startswith("/"): + self.src_dict[key] = "/" + self.src_dict[key] + TangoReadableDevice.__init__(self, trl, name) self._set_success = True # -------------------------------------------------------------------- def register_signals(self) -> None: self.position = tango_signal_rw( - float, self.trl + "/position", device_proxy=self.proxy + float, self.trl + self.src_dict["position"], device_proxy=self.proxy ) self.baserate = tango_signal_rw( - int, self.trl + "/baserate", device_proxy=self.proxy + int, self.trl + self.src_dict["baserate"], device_proxy=self.proxy ) self.slewrate = tango_signal_rw( - int, self.trl + "/slewrate", device_proxy=self.proxy + int, self.trl + self.src_dict["slewrate"], device_proxy=self.proxy ) self.conversion = tango_signal_rw( - float, self.trl + "/conversion", device_proxy=self.proxy + float, self.trl + self.src_dict["conversion"], device_proxy=self.proxy ) self.acceleration = tango_signal_rw( - int, self.trl + "/acceleration", device_proxy=self.proxy + int, self.trl + self.src_dict["acceleration"], device_proxy=self.proxy ) self.set_readable_signals( @@ -46,8 +72,10 @@ def register_signals(self) -> None: config=[self.baserate, self.slewrate, self.conversion, self.acceleration], ) - self._stop = tango_signal_x(self.trl + "/stopmove", self.proxy) - self._state = tango_signal_r(DevState, self.trl + "/state", self.proxy) + self._stop = tango_signal_x(self.trl + self.src_dict["stop"], self.proxy) + self._state = tango_signal_r( + DevState, self.trl + self.src_dict["state"], self.proxy + ) self.set_name(self.name) diff --git a/src/ophyd_async/tango/device_controllers/sis3820.py b/src/ophyd_async/tango/device_controllers/sis3820.py index 4df4b9c8e8..7920d555dd 100644 --- a/src/ophyd_async/tango/device_controllers/sis3820.py +++ b/src/ophyd_async/tango/device_controllers/sis3820.py @@ -11,23 +11,43 @@ # -------------------------------------------------------------------- class SIS3820Counter(TangoReadableDevice): # Triggerable + src_dict: dict = { + "counts": "/counts", + "offset": "/offset", + "reset": "/reset", + } + trl: str + name: str + # -------------------------------------------------------------------- - def __init__(self, trl: str, name: str = "") -> None: + def __init__(self, trl: str, name: str = "", sources: dict = None) -> None: + if sources is None: + sources = {} + self.src_dict["counts"] = sources.get("counts", "/counts") + self.src_dict["offset"] = sources.get("offset", "/offset") + self.src_dict["reset"] = sources.get("reset", "/reset") + + for key in self.src_dict: + if not self.src_dict[key].startswith("/"): + self.src_dict[key] = "/" + self.src_dict[key] + TangoReadableDevice.__init__(self, trl, name) self._set_success = True # -------------------------------------------------------------------- def register_signals(self) -> None: self.counts = tango_signal_rw( - float, self.trl + "/counts", device_proxy=self.proxy + float, self.trl + self.src_dict["counts"], device_proxy=self.proxy ) self.offset = tango_signal_rw( - float, self.trl + "/offset", device_proxy=self.proxy + float, self.trl + self.src_dict["offset"], device_proxy=self.proxy ) self.set_readable_signals(read_uncached=[self.counts], config=[self.offset]) - self.reset = tango_signal_x(self.trl + "/reset", device_proxy=self.proxy) + self.reset = tango_signal_x( + self.trl + self.src_dict["reset"], device_proxy=self.proxy + ) self.set_name(self.name) From fc88243c3f032ecf479a08811c1216d83ac903d6 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 29 May 2024 11:07:34 +0200 Subject: [PATCH 030/141] get_dtype can now handle strings --- src/ophyd_async/core/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ophyd_async/core/utils.py b/src/ophyd_async/core/utils.py index ad70bcb62e..fb889105cd 100644 --- a/src/ophyd_async/core/utils.py +++ b/src/ophyd_async/core/utils.py @@ -111,6 +111,8 @@ def get_dtype(typ: Type) -> Optional[np.dtype]: # datatype = numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] # so extract numpy.float64 from it return np.dtype(typ.__args__[1].__args__[0]) # type: ignore + elif typ == str: + return np.dtype("U") # Unicode string type return None From b002b1409a922cdfb8cb954ec620181a3b871902 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 30 May 2024 13:16:48 +0200 Subject: [PATCH 031/141] . --- src/ophyd_async/tango/device_controllers/dgg2.py | 2 +- src/ophyd_async/tango/device_controllers/omsvme58.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ophyd_async/tango/device_controllers/dgg2.py b/src/ophyd_async/tango/device_controllers/dgg2.py index c8cdfbc2f9..9e4e9d433f 100644 --- a/src/ophyd_async/tango/device_controllers/dgg2.py +++ b/src/ophyd_async/tango/device_controllers/dgg2.py @@ -121,7 +121,7 @@ def update_watchers(remaining_time: float) -> None: # -------------------------------------------------------------------- def trigger(self) -> AsyncStatus: watchers: List[Callable] = [] - return AsyncStatus(self._trigger(watchers), watchers) + return AsyncStatus(self._trigger(watchers)) # -------------------------------------------------------------------- def prepare(self, p_time: float) -> AsyncStatus: diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index 09d06c6109..b0852061e3 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -140,4 +140,4 @@ async def locate(self) -> Location: # -------------------------------------------------------------------- def stop(self, success: bool = False) -> AsyncStatus: self._set_success = success - return AsyncStatus(self._stop.trigger()) + return self._stop.trigger() From febd81f187b58648f245893abfe960c3a270f8aa Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 25 Jun 2024 10:05:16 +0200 Subject: [PATCH 032/141] Reversed change to core util. --- src/ophyd_async/core/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ophyd_async/core/utils.py b/src/ophyd_async/core/utils.py index fb889105cd..ad70bcb62e 100644 --- a/src/ophyd_async/core/utils.py +++ b/src/ophyd_async/core/utils.py @@ -111,8 +111,6 @@ def get_dtype(typ: Type) -> Optional[np.dtype]: # datatype = numpy.ndarray[typing.Any, numpy.dtype[numpy.float64]] # so extract numpy.float64 from it return np.dtype(typ.__args__[1].__args__[0]) # type: ignore - elif typ == str: - return np.dtype("U") # Unicode string type return None From 2e79e5e73018b153f5aecaa71d4ad4915d74364d Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 25 Jun 2024 15:55:23 +0200 Subject: [PATCH 033/141] Minor changes --- src/ophyd_async/tango/device_controllers/sis3820.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ophyd_async/tango/device_controllers/sis3820.py b/src/ophyd_async/tango/device_controllers/sis3820.py index 7920d555dd..3c11eb3b67 100644 --- a/src/ophyd_async/tango/device_controllers/sis3820.py +++ b/src/ophyd_async/tango/device_controllers/sis3820.py @@ -2,15 +2,14 @@ from typing import Dict -# from bluesky.protocols import Triggerable -from bluesky.protocols import Reading +from bluesky.protocols import Reading, Triggerable from ophyd_async.core import AsyncStatus from ophyd_async.tango import TangoReadableDevice, tango_signal_rw, tango_signal_x # -------------------------------------------------------------------- -class SIS3820Counter(TangoReadableDevice): # Triggerable +class SIS3820Counter(TangoReadableDevice, Triggerable): src_dict: dict = { "counts": "/counts", "offset": "/offset", From 5f8c1113e64d83c054099a9cb63b24897edea475 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 26 Jun 2024 15:03:09 +0200 Subject: [PATCH 034/141] updated --- .../tango/_backend/_signal_backend.py | 4 ++-- .../tango/base_devices/base_device.py | 20 +++++++------------ .../tango/device_controllers/omsvme58.py | 2 +- .../tango/device_controllers/tango_mover.py | 2 +- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_signal_backend.py b/src/ophyd_async/tango/_backend/_signal_backend.py index 0db6d0d0af..0aed066a35 100644 --- a/src/ophyd_async/tango/_backend/_signal_backend.py +++ b/src/ophyd_async/tango/_backend/_signal_backend.py @@ -3,13 +3,13 @@ from typing import Callable, Optional from ophyd_async.core import SignalR, SignalRW, SignalW, SignalX, T -from ophyd_async.core.signal import add_timeout +from ophyd_async.core.signal import _add_timeout # -------------------------------------------------------------------- # from tango attributes one can get setvalue, so we extend SignalRW and SignalW class SignalWithSetpoint: - @add_timeout + @_add_timeout async def get_setpoint(self, cached: Optional[bool] = None) -> T: """The last written value to TRL""" return await self._backend_or_cache(cached).get_w_value() diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index d96cdabcd6..4f9fb85e7d 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -24,25 +24,19 @@ def __init__(self, trl: str, name="") -> None: self.proxy: DeviceProxy = None StandardReadable.__init__(self, name=name) - # # -------------------------------------------------------------------- - # def __await__(self): - # async def closure(): - # self.proxy = await DeviceProxy(self.trl) - # self.register_signals() - # - # return self - # - # return closure().__await__() - - # -------------------------------------------------------------------- - async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + async def connect( + self, + mock: bool = False, + timeout: float = DEFAULT_TIMEOUT, + force_reconnect: bool = False, + ): async def closure(): self.proxy = await DeviceProxy(self.trl) self.register_signals() return self await closure() - await super().connect(sim=sim, timeout=timeout) + await super().connect(mock=mock, timeout=timeout) # -------------------------------------------------------------------- @abstractmethod diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index b0852061e3..8ed2ba8c2f 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -129,7 +129,7 @@ def update_watchers(current_position: float) -> None: def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus: watchers: List[Callable] = [] coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout) - return AsyncStatus(coro, watchers) + return AsyncStatus(coro) # -------------------------------------------------------------------- async def locate(self) -> Location: diff --git a/src/ophyd_async/tango/device_controllers/tango_mover.py b/src/ophyd_async/tango/device_controllers/tango_mover.py index 6525d4995c..36add7627a 100644 --- a/src/ophyd_async/tango/device_controllers/tango_mover.py +++ b/src/ophyd_async/tango/device_controllers/tango_mover.py @@ -214,7 +214,7 @@ def update_watchers(current_position: float) -> None: def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus: watchers: List[Callable] = [] coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout) - return AsyncStatus(coro, watchers) + return AsyncStatus(coro) # -------------------------------------------------------------------- async def locate(self) -> Location: From 50a310c44d5031ecdf01113e5c3e04be118d8754 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 27 Jun 2024 09:38:09 +0200 Subject: [PATCH 035/141] Removed sardana example devices --- src/ophyd_async/tango/sardana/__init__.py | 3 - src/ophyd_async/tango/sardana/motor.py | 95 ----------------------- 2 files changed, 98 deletions(-) delete mode 100644 src/ophyd_async/tango/sardana/__init__.py delete mode 100644 src/ophyd_async/tango/sardana/motor.py diff --git a/src/ophyd_async/tango/sardana/__init__.py b/src/ophyd_async/tango/sardana/__init__.py deleted file mode 100644 index d68bafbe32..0000000000 --- a/src/ophyd_async/tango/sardana/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from ophyd_async.tango.sardana.motor import SardanaMotor - -__all__ = ("SardanaMotor",) diff --git a/src/ophyd_async/tango/sardana/motor.py b/src/ophyd_async/tango/sardana/motor.py deleted file mode 100644 index 520d49ebba..0000000000 --- a/src/ophyd_async/tango/sardana/motor.py +++ /dev/null @@ -1,95 +0,0 @@ -from __future__ import annotations - -import asyncio -import time -from typing import Callable, List, Optional - -from bluesky.protocols import Locatable, Location, Stoppable - -from ophyd_async.core import AsyncStatus -from ophyd_async.tango import ( - TangoReadableDevice, - tango_signal_r, - tango_signal_rw, - tango_signal_x, -) -from tango import DevState - - -# -------------------------------------------------------------------- -class SardanaMotor(TangoReadableDevice, Locatable, Stoppable): - # -------------------------------------------------------------------- - def __init__(self, trl: str, name="") -> None: - TangoReadableDevice.__init__(self, trl, name) - self._set_success = True - - # -------------------------------------------------------------------- - def register_signals(self): - self.position = tango_signal_rw( - float, self.trl + "/Position", device_proxy=self.proxy - ) - self.baserate = tango_signal_rw( - float, self.trl + "/Base_rate", device_proxy=self.proxy - ) - self.velocity = tango_signal_rw( - float, self.trl + "/Velocity", device_proxy=self.proxy - ) - self.acceleration = tango_signal_rw( - float, self.trl + "/Acceleration", device_proxy=self.proxy - ) - self.deceleration = tango_signal_rw( - float, self.trl + "/Deceleration", device_proxy=self.proxy - ) - - self.set_readable_signals( - read_uncached=[self.position], - config=[self.baserate, self.velocity, self.acceleration, self.deceleration], - ) - - self._stop = tango_signal_x(self.trl + "/Stop", self.proxy) - self._state = tango_signal_r(DevState, self.trl + "/State", self.proxy) - - # -------------------------------------------------------------------- - async def _move(self, new_position: float, watchers: List[Callable] = []): - self._set_success = True - start = time.monotonic() - start_position = await self.position.get_value() - - def update_watchers(current_position: float): - for watcher in watchers: - watcher( - name=self.name, - current=current_position, - initial=start_position, - target=new_position, - time_elapsed=time.monotonic() - start, - ) - - # raise RuntimeError("Test") - self.position.subscribe_value(update_watchers) - try: - await self.position.set(new_position) - await asyncio.sleep(0.1) - while await self._state.get_value() == DevState.MOVING: - await asyncio.sleep(0.1) - finally: - self.position.clear_sub(update_watchers) - if not self._set_success: - raise RuntimeError("Motor was stopped") - - # -------------------------------------------------------------------- - def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus: - watchers: List[Callable] = [] - coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout) - return AsyncStatus(coro, watchers) - - # -------------------------------------------------------------------- - async def locate(self) -> Location: - pass - - # -------------------------------------------------------------------- - async def stop(self, success=False): - self._set_success = success - # Put with completion will never complete as we are waiting for completion on - # the move above, so need to pass wait=False - await self._stop.execute(wait=False) From c3333243313e93641359a8d9345c8734b634e20e Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 27 Jun 2024 09:40:31 +0200 Subject: [PATCH 036/141] Removed use of a specific tango signal type. tango signals in devices should use a core signal front-end with a tango backend. --- .../tango/_backend/_signal_backend.py | 52 +++--- src/ophyd_async/tango/signal/signal.py | 152 ++++++++++-------- 2 files changed, 115 insertions(+), 89 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_signal_backend.py b/src/ophyd_async/tango/_backend/_signal_backend.py index 0aed066a35..be339d9fb4 100644 --- a/src/ophyd_async/tango/_backend/_signal_backend.py +++ b/src/ophyd_async/tango/_backend/_signal_backend.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import Callable, Optional +from typing import Optional -from ophyd_async.core import SignalR, SignalRW, SignalW, SignalX, T +from ophyd_async.core import T from ophyd_async.core.signal import _add_timeout @@ -23,33 +23,33 @@ def is_cachable(self) -> T: return self._backend.is_cachable() -# -------------------------------------------------------------------- -class TangoSignalW(SignalW[T], CachableOrNot, SignalWithSetpoint): - # -------------------------------------------------------------------- - @property - def source(self) -> Callable[[], str]: - return self._backend.source +# # -------------------------------------------------------------------- +# class TangoSignalW(SignalW[T], CachableOrNot, SignalWithSetpoint): +# # -------------------------------------------------------------------- +# @property +# def source(self) -> Callable[[], str]: +# return self._backend.source -# -------------------------------------------------------------------- -class TangoSignalRW(SignalRW[T], CachableOrNot, SignalWithSetpoint): - # -------------------------------------------------------------------- - @property - def source(self) -> Callable[[], str]: - return self._backend.source +# # -------------------------------------------------------------------- +# class TangoSignalRW(SignalRW[T], CachableOrNot, SignalWithSetpoint): +# # -------------------------------------------------------------------- +# @property +# def source(self) -> Callable[[], str]: +# return self._backend.source -# -------------------------------------------------------------------- -class TangoSignalR(SignalR[T], CachableOrNot): - # -------------------------------------------------------------------- - @property - def source(self) -> Callable[[], str]: - return self._backend.source +# # -------------------------------------------------------------------- +# class TangoSignalR(SignalR[T], CachableOrNot): +# # -------------------------------------------------------------------- +# @property +# def source(self) -> Callable[[], str]: +# return self._backend.source -# -------------------------------------------------------------------- -class TangoSignalX(SignalX, CachableOrNot): - # -------------------------------------------------------------------- - @property - def source(self) -> Callable[[], str]: - return self._backend.source +# # -------------------------------------------------------------------- +# class TangoSignalX(SignalX, CachableOrNot): +# # -------------------------------------------------------------------- +# @property +# def source(self) -> Callable[[], str]: +# return self._backend.source diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 2f815eca6e..cf05574b4b 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -2,28 +2,26 @@ from __future__ import annotations -from typing import Optional, Type, Union +from typing import Optional, Type -from ophyd_async.core import T +from ophyd_async.core import SignalR, SignalRW, SignalW, SignalX, T from ophyd_async.tango._backend import ( TangoSignalBackend, - TangoSignalR, - TangoSignalRW, - TangoSignalW, - TangoSignalX, TangoTransport, ) -from tango import AttrWriteType, CmdArgType -from tango import DeviceProxy as SyncDeviceProxy +from ophyd_async.utils import DEFAULT_TIMEOUT from tango.asyncio import DeviceProxy -__all__ = ( - "tango_signal_rw", - "tango_signal_r", - "tango_signal_w", - "tango_signal_x", - "tango_signal_auto", -) +__all__ = ("tango_signal_rw", "tango_signal_r", "tango_signal_w", "tango_signal_x") + + +def _make_backend( + datatype: Optional[Type[T]], + read_trl: str, + write_trl: str, + device_proxy: Optional[DeviceProxy] = None, +) -> TangoSignalBackend: + return TangoTransport(datatype, read_trl, write_trl, device_proxy) # -------------------------------------------------------------------- @@ -32,7 +30,9 @@ def tango_signal_rw( read_trl: str, write_trl: Optional[str] = None, device_proxy: Optional[DeviceProxy] = None, -) -> TangoSignalRW[T]: + timeout: float = DEFAULT_TIMEOUT, + name: str = "", +) -> SignalRW[T]: """Create a `SignalRW` backed by 1 or 2 Tango Attribute/Command Parameters @@ -45,15 +45,23 @@ def tango_signal_rw( If given, use this Attribute/Command to write to, otherwise use read_trl device_proxy: If given, this DeviceProxy will be used + timeout: + The timeout for the read and write operations + name: + The name of the Signal """ - backend = TangoTransport(datatype, read_trl, write_trl or read_trl, device_proxy) - return TangoSignalRW(backend) + backend = _make_backend(datatype, read_trl, write_trl or read_trl, device_proxy) + return SignalRW(backend, timeout=timeout, name=name) # -------------------------------------------------------------------- def tango_signal_r( - datatype: Type[T], read_trl: str, device_proxy: Optional[DeviceProxy] = None -) -> TangoSignalR[T]: + datatype: Type[T], + read_trl: str, + device_proxy: Optional[DeviceProxy] = None, + timeout: float = DEFAULT_TIMEOUT, + name: str = "", +) -> SignalR[T]: """Create a `SignalR` backed by 1 Tango Attribute/Command Parameters @@ -64,15 +72,23 @@ def tango_signal_r( The Attribute/Command to read and monitor device_proxy: If given, this DeviceProxy will be used + timeout: + The timeout for the read operation + name: + The name of the Signal """ - backend = TangoTransport(datatype, read_trl, read_trl, device_proxy) - return TangoSignalR(backend) + backend = _make_backend(datatype, read_trl, read_trl, device_proxy) + return SignalR(backend, timeout=timeout, name=name) # -------------------------------------------------------------------- def tango_signal_w( - datatype: Type[T], write_trl: str, device_proxy: Optional[DeviceProxy] = None -) -> TangoSignalW[T]: + datatype: Type[T], + write_trl: str, + device_proxy: Optional[DeviceProxy] = None, + timeout: float = DEFAULT_TIMEOUT, + name: str = "", +) -> SignalW[T]: """Create a `TangoSignalW` backed by 1 Tango Attribute/Command Parameters @@ -83,15 +99,22 @@ def tango_signal_w( The Attribute/Command to write to device_proxy: If given, this DeviceProxy will be used + timeout: + The timeout for the write operation + name: + The name of the Signal """ - backend = TangoTransport(datatype, write_trl, write_trl, device_proxy) - return TangoSignalW(backend) + backend = _make_backend(datatype, write_trl, write_trl, device_proxy) + return SignalW(backend, timeout=timeout, name=name) # -------------------------------------------------------------------- def tango_signal_x( - write_trl: str, device_proxy: Optional[DeviceProxy] = None -) -> TangoSignalX: + write_trl: str, + device_proxy: Optional[DeviceProxy] = None, + timeout: float = DEFAULT_TIMEOUT, + name: str = "", +) -> SignalX: """Create a `SignalX` backed by 1 Tango Attribute/Command Parameters @@ -100,42 +123,45 @@ def tango_signal_x( The Attribute/Command to write its initial value to on execute device_proxy: If given, this DeviceProxy will be used + timeout: + The timeout for the command operation + name: + The name of the Signal """ - backend: TangoSignalBackend = TangoTransport( - None, write_trl, write_trl, device_proxy - ) - return TangoSignalX(backend) + backend = _make_backend(None, write_trl, write_trl, device_proxy) + return SignalX(backend, timeout=timeout, name=name) # -------------------------------------------------------------------- -def tango_signal_auto( - datatype: Type[T], full_trl: str, device_proxy: Optional[DeviceProxy] = None -) -> Union[TangoSignalW, TangoSignalX, TangoSignalR, TangoSignalRW]: - backend: TangoSignalBackend = TangoTransport( - datatype, full_trl, full_trl, device_proxy - ) - - device_trl, tr_name = full_trl.rsplit("/", 1) - device_proxy = SyncDeviceProxy(device_trl) - if tr_name in device_proxy.get_attribute_list(): - config = device_proxy.get_attribute_config(tr_name) - if config.writable in [AttrWriteType.READ_WRITE, AttrWriteType.READ_WITH_WRITE]: - return TangoSignalRW(backend) - elif config.writable == AttrWriteType.READ: - return TangoSignalR(backend) - else: - return TangoSignalW(backend) - - if tr_name in device_proxy.get_command_list(): - config = device_proxy.get_command_config(tr_name) - if config.in_type == CmdArgType.DevVoid: - return TangoSignalX(backend) - elif config.out_type != CmdArgType.DevVoid: - return TangoSignalRW(backend) - else: - return TangoSignalX(backend) - - if tr_name in device_proxy.get_pipe_list(): - raise NotImplementedError("Pipes are not supported") - - raise RuntimeError(f"Cannot find {tr_name} in {device_trl}") +# def tango_signal_auto( +# datatype: Type[T], full_trl: str, device_proxy: Optional[DeviceProxy] = None +# ) -> Union[TangoSignalW, TangoSignalX, TangoSignalR, TangoSignalRW]: +# backend: TangoSignalBackend = TangoTransport( +# datatype, full_trl, full_trl, device_proxy +# ) + +# device_trl, tr_name = full_trl.rsplit("/", 1) +# device_proxy = SyncDeviceProxy(device_trl) +# if tr_name in device_proxy.get_attribute_list(): +# config = device_proxy.get_attribute_config(tr_name) +# if config.writable in [AttrWriteType.READ_WRITE, +# AttrWriteType.READ_WITH_WRITE]: +# return TangoSignalRW(backend) +# elif config.writable == AttrWriteType.READ: +# return TangoSignalR(backend) +# else: +# return TangoSignalW(backend) + +# if tr_name in device_proxy.get_command_list(): +# config = device_proxy.get_command_config(tr_name) +# if config.in_type == CmdArgType.DevVoid: +# return TangoSignalX(backend) +# elif config.out_type != CmdArgType.DevVoid: +# return TangoSignalRW(backend) +# else: +# return TangoSignalX(backend) + +# if tr_name in device_proxy.get_pipe_list(): +# raise NotImplementedError("Pipes are not supported") + +# raise RuntimeError(f"Cannot find {tr_name} in {device_trl}") From 6c511c3a44370b6e3247633e2235ccf9f5db2015 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 27 Jun 2024 13:44:54 +0200 Subject: [PATCH 037/141] Test device for new repo structure --- .../tango/device_controllers/new_sis3820.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/ophyd_async/tango/device_controllers/new_sis3820.py diff --git a/src/ophyd_async/tango/device_controllers/new_sis3820.py b/src/ophyd_async/tango/device_controllers/new_sis3820.py new file mode 100644 index 0000000000..34e7b49622 --- /dev/null +++ b/src/ophyd_async/tango/device_controllers/new_sis3820.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from typing import Dict + +from bluesky.protocols import Reading, Triggerable + +from ophyd_async.core import ( + AsyncStatus, + ConfigSignal, + HintedSignal, +) +from ophyd_async.tango import TangoReadableDevice, tango_signal_rw, tango_signal_x +from tango import DeviceProxy + + +# -------------------------------------------------------------------- +class SIS3820Counter(TangoReadableDevice, Triggerable): + src_dict: dict + trl: str + proxy: DeviceProxy + name: str + + # -------------------------------------------------------------------- + def __init__(self, trl: str, name: str = "", sources: dict = None) -> None: + if sources is None: + sources = {} + self.src_dict["counts"] = sources.get("counts", "/counts") + self.src_dict["offset"] = sources.get("offset", "/offset") + self.src_dict["reset"] = sources.get("reset", "/reset") + + for key in self.src_dict: + if not self.src_dict[key].startswith("/"): + self.src_dict[key] = "/" + self.src_dict[key] + + with self.add_children_as_readables(HintedSignal): + self.counts = tango_signal_rw( + float, self.trl + self.src_dict["counts"], device_proxy=self.proxy + ) + with self.add_children_as_readables(ConfigSignal): + self.offset = tango_signal_rw( + float, self.trl + self.src_dict["offset"], device_proxy=self.proxy + ) + + self.reset = tango_signal_x( + self.trl + self.src_dict["reset"], device_proxy=self.proxy + ) + + TangoReadableDevice.__init__(self, trl, name) + self._set_success = True + + async def read(self) -> Dict[str, Reading]: + ret = await super().read() + return ret + + def trigger(self) -> AsyncStatus: + return AsyncStatus(self._trigger()) + + async def _trigger(self) -> None: + await self.reset.trigger() From f3789339e2f95c02e749b41b5a9f2ad445523066 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 27 Jun 2024 13:46:13 +0200 Subject: [PATCH 038/141] Simplifying tango base device --- .../tango/base_devices/base_device.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index 4f9fb85e7d..469f0c5d9d 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -1,8 +1,9 @@ from __future__ import annotations from abc import abstractmethod +from typing import Optional -from ophyd_async.core import DEFAULT_TIMEOUT, AsyncStatus, StandardReadable +from ophyd_async.core import DEFAULT_TIMEOUT, StandardReadable from tango.asyncio import DeviceProxy __all__ = ("TangoReadableDevice",) @@ -21,7 +22,7 @@ class TangoReadableDevice(StandardReadable): # -------------------------------------------------------------------- def __init__(self, trl: str, name="") -> None: self.trl = trl - self.proxy: DeviceProxy = None + self.proxy: Optional[DeviceProxy] = None StandardReadable.__init__(self, name=name) async def connect( @@ -32,7 +33,6 @@ async def connect( ): async def closure(): self.proxy = await DeviceProxy(self.trl) - self.register_signals() return self await closure() @@ -45,16 +45,16 @@ def register_signals(self): This method should be used to register signals """ - # -------------------------------------------------------------------- - @AsyncStatus.wrap - async def stage(self) -> None: - for sig in self._readables + self._configurables: - if hasattr(sig, "is_cachable") and sig.is_cachable(): - await sig.stage().task - - # -------------------------------------------------------------------- - @AsyncStatus.wrap - async def unstage(self) -> None: - for sig in self._readables + self._configurables: - if hasattr(sig, "is_cachable") and sig.is_cachable(): - await sig.unstage().task + # # -------------------------------------------------------------------- + # @AsyncStatus.wrap + # async def stage(self) -> None: + # for sig in self._readables + self._configurables: + # if hasattr(sig, "is_cachable") and sig.is_cachable(): + # await sig.stage().task + + # # -------------------------------------------------------------------- + # @AsyncStatus.wrap + # async def unstage(self) -> None: + # for sig in self._readables + self._configurables: + # if hasattr(sig, "is_cachable") and sig.is_cachable(): + # await sig.unstage().task From 3a91768c13739d506a5e83ee3b3e63232117d762 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 27 Jun 2024 14:39:57 +0200 Subject: [PATCH 039/141] Removing some frontend dependencies. --- src/ophyd_async/tango/__init__.py | 4 ++-- src/ophyd_async/tango/_backend/__init__.py | 14 ++------------ src/ophyd_async/tango/signal/__init__.py | 4 ++-- src/ophyd_async/tango/signal/signal.py | 2 +- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py index 88276fc73c..a9feff000d 100644 --- a/src/ophyd_async/tango/__init__.py +++ b/src/ophyd_async/tango/__init__.py @@ -1,6 +1,6 @@ from ophyd_async.tango.base_devices import TangoReadableDevice from ophyd_async.tango.signal import ( - tango_signal_auto, +# tango_signal_auto, tango_signal_r, tango_signal_rw, tango_signal_w, @@ -12,6 +12,6 @@ "tango_signal_rw", "tango_signal_w", "tango_signal_x", - "tango_signal_auto", +# "tango_signal_auto", "TangoReadableDevice", ] diff --git a/src/ophyd_async/tango/_backend/__init__.py b/src/ophyd_async/tango/_backend/__init__.py index 1cd76bef80..e7073de20e 100644 --- a/src/ophyd_async/tango/_backend/__init__.py +++ b/src/ophyd_async/tango/_backend/__init__.py @@ -1,19 +1,9 @@ -from ophyd_async.tango._backend._signal_backend import ( - TangoSignalR, - TangoSignalRW, - TangoSignalW, - TangoSignalX, -) from ophyd_async.tango._backend._tango_transport import ( TangoSignalBackend, - TangoTransport, + TangoTransport ) __all__ = ( "TangoTransport", - "TangoSignalBackend", - "TangoSignalW", - "TangoSignalRW", - "TangoSignalR", - "TangoSignalX", + "TangoSignalBackend" ) diff --git a/src/ophyd_async/tango/signal/__init__.py b/src/ophyd_async/tango/signal/__init__.py index e006b3645c..b55f7e5a1d 100644 --- a/src/ophyd_async/tango/signal/__init__.py +++ b/src/ophyd_async/tango/signal/__init__.py @@ -1,5 +1,5 @@ from .signal import ( - tango_signal_auto, +# tango_signal_auto, tango_signal_r, tango_signal_rw, tango_signal_w, @@ -11,5 +11,5 @@ "tango_signal_rw", "tango_signal_w", "tango_signal_x", - "tango_signal_auto", +# "tango_signal_auto", ) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index cf05574b4b..6e47551706 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -9,7 +9,7 @@ TangoSignalBackend, TangoTransport, ) -from ophyd_async.utils import DEFAULT_TIMEOUT +from ophyd_async.core.utils import DEFAULT_TIMEOUT from tango.asyncio import DeviceProxy __all__ = ("tango_signal_rw", "tango_signal_r", "tango_signal_w", "tango_signal_x") From a43f087200a5dcc5ad06c770719cba555afc6ff7 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 27 Jun 2024 15:57:50 +0200 Subject: [PATCH 040/141] new_sis3820 device appears to be working correctly. In this version an ophyd generic signal is the frontend of each tango signal and all the tango specific code is contained in the backend. --- src/ophyd_async/tango/_backend/__init__.py | 2 -- .../tango/_backend/_tango_transport.py | 36 +++++-------------- .../tango/base_devices/base_device.py | 10 ++---- .../tango/device_controllers/new_sis3820.py | 15 ++++---- src/ophyd_async/tango/signal/signal.py | 7 ++-- 5 files changed, 20 insertions(+), 50 deletions(-) diff --git a/src/ophyd_async/tango/_backend/__init__.py b/src/ophyd_async/tango/_backend/__init__.py index e7073de20e..8938071c92 100644 --- a/src/ophyd_async/tango/_backend/__init__.py +++ b/src/ophyd_async/tango/_backend/__init__.py @@ -1,9 +1,7 @@ from ophyd_async.tango._backend._tango_transport import ( - TangoSignalBackend, TangoTransport ) __all__ = ( "TangoTransport", - "TangoSignalBackend" ) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 93cce421b4..86802eec77 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -7,7 +7,7 @@ from typing import Dict, Optional, Type, Union import numpy as np -from bluesky.protocols import Descriptor, Reading +from bluesky.protocols import Descriptor, Reading, DataKey from ophyd_async.core import ( DEFAULT_TIMEOUT, @@ -35,15 +35,13 @@ ) from tango.utils import is_array, is_binary, is_bool, is_float, is_int, is_str -__all__ = ("TangoTransport", "TangoSignalBackend") +__all__ = ["TangoTransport"] # time constant to wait for timeout A_BIT = 1e-5 # -------------------------------------------------------------------- - - def ensure_proper_executor(func): @functools.wraps(func) async def wrapper(self, *args, **kwargs): @@ -54,20 +52,6 @@ async def wrapper(self, *args, **kwargs): return wrapper - -# -------------------------------------------------------------------- -class TangoSignalBackend(SignalBackend[T]): - # -------------------------------------------------------------------- - @abstractmethod - async def get_w_value(self) -> T: - """The last written value""" - - # -------------------------------------------------------------------- - @abstractmethod - def get_signal_auto(self) -> str: - """return signal type, passing to tango attribute/command""" - - # -------------------------------------------------------------------- def get_python_type(tango_type) -> tuple[bool, T, str]: array = is_array(tango_type) @@ -422,7 +406,7 @@ async def get_tango_trl( # -------------------------------------------------------------------- -class TangoTransport(TangoSignalBackend[T]): +class TangoTransport(SignalBackend[T]): def __init__( self, datatype: Optional[Type[T]], @@ -438,13 +422,11 @@ def __init__( write_trl: device_proxy, } self.trl_configs: Dict[str, AttributeInfoEx] = {} - # self.source = f"{self.read_trl}" self.descriptor: Descriptor = {} # type: ignore # -------------------------------------------------------------------- - @property - def source(self) -> str: - return f"{self.read_trl}" + def source(self, name: str) -> str: + return self.read_trl # -------------------------------------------------------------------- async def _connect_and_store_config(self, trl): @@ -471,11 +453,11 @@ async def connect(self, timeout: float = DEFAULT_TIMEOUT): ) # -------------------------------------------------------------------- - async def put(self, write_value: Optional[T], wait=True, timeout=None): - await self.proxies[self.write_trl].put(write_value, wait, timeout) + async def put(self, value: Optional[T], wait=True, timeout=None): + await self.proxies[self.write_trl].put(value, wait, timeout) # -------------------------------------------------------------------- - async def get_descriptor(self, source: str) -> Descriptor: + async def get_datakey(self, source: str) -> DataKey: return self.descriptor # -------------------------------------------------------------------- @@ -487,7 +469,7 @@ async def get_value(self) -> T: return await self.proxies[self.write_trl].get() # -------------------------------------------------------------------- - async def get_w_value(self) -> T: + async def get_setpoint(self) -> T: return await self.proxies[self.write_trl].get_w_value() # -------------------------------------------------------------------- diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index 469f0c5d9d..b55a083b0f 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -18,6 +18,9 @@ class TangoReadableDevice(StandardReadable): new_device = await TangoDevice() """ + src_dict: dict = {} + trl: str = "" + proxy: Optional[DeviceProxy] = None # -------------------------------------------------------------------- def __init__(self, trl: str, name="") -> None: @@ -38,13 +41,6 @@ async def closure(): await closure() await super().connect(mock=mock, timeout=timeout) - # -------------------------------------------------------------------- - @abstractmethod - def register_signals(self): - """ - This method should be used to register signals - """ - # # -------------------------------------------------------------------- # @AsyncStatus.wrap # async def stage(self) -> None: diff --git a/src/ophyd_async/tango/device_controllers/new_sis3820.py b/src/ophyd_async/tango/device_controllers/new_sis3820.py index 34e7b49622..c678d69611 100644 --- a/src/ophyd_async/tango/device_controllers/new_sis3820.py +++ b/src/ophyd_async/tango/device_controllers/new_sis3820.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Dict +from typing import Dict, Optional from bluesky.protocols import Reading, Triggerable @@ -10,27 +10,24 @@ HintedSignal, ) from ophyd_async.tango import TangoReadableDevice, tango_signal_rw, tango_signal_x -from tango import DeviceProxy # -------------------------------------------------------------------- class SIS3820Counter(TangoReadableDevice, Triggerable): - src_dict: dict - trl: str - proxy: DeviceProxy - name: str # -------------------------------------------------------------------- def __init__(self, trl: str, name: str = "", sources: dict = None) -> None: if sources is None: sources = {} - self.src_dict["counts"] = sources.get("counts", "/counts") - self.src_dict["offset"] = sources.get("offset", "/offset") - self.src_dict["reset"] = sources.get("reset", "/reset") + self.src_dict["counts"] = sources.get("counts", "/Counts") + self.src_dict["offset"] = sources.get("offset", "/Offset") + self.src_dict["reset"] = sources.get("reset", "/Reset") for key in self.src_dict: if not self.src_dict[key].startswith("/"): self.src_dict[key] = "/" + self.src_dict[key] + + self.trl = trl with self.add_children_as_readables(HintedSignal): self.counts = tango_signal_rw( diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 6e47551706..9cb890424f 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -5,10 +5,7 @@ from typing import Optional, Type from ophyd_async.core import SignalR, SignalRW, SignalW, SignalX, T -from ophyd_async.tango._backend import ( - TangoSignalBackend, - TangoTransport, -) +from ophyd_async.tango._backend import TangoTransport from ophyd_async.core.utils import DEFAULT_TIMEOUT from tango.asyncio import DeviceProxy @@ -20,7 +17,7 @@ def _make_backend( read_trl: str, write_trl: str, device_proxy: Optional[DeviceProxy] = None, -) -> TangoSignalBackend: +) -> TangoTransport: return TangoTransport(datatype, read_trl, write_trl, device_proxy) From 517f98386a2c78534dbed0c1d03c3bb0e01ba243 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 28 Jun 2024 09:24:10 +0200 Subject: [PATCH 041/141] sis3820 rewritten for more generic format --- src/ophyd_async/tango/_backend/__init__.py | 8 +-- .../tango/_backend/_tango_transport.py | 4 +- .../tango/base_devices/base_device.py | 2 +- .../tango/device_controllers/new_sis3820.py | 56 ------------------- .../tango/device_controllers/sis3820.py | 46 +++++++-------- src/ophyd_async/tango/signal/signal.py | 2 +- 6 files changed, 26 insertions(+), 92 deletions(-) delete mode 100644 src/ophyd_async/tango/device_controllers/new_sis3820.py diff --git a/src/ophyd_async/tango/_backend/__init__.py b/src/ophyd_async/tango/_backend/__init__.py index 8938071c92..81b66e539d 100644 --- a/src/ophyd_async/tango/_backend/__init__.py +++ b/src/ophyd_async/tango/_backend/__init__.py @@ -1,7 +1,3 @@ -from ophyd_async.tango._backend._tango_transport import ( - TangoTransport -) +from ophyd_async.tango._backend._tango_transport import TangoTransport -__all__ = ( - "TangoTransport", -) +__all__ = ("TangoTransport",) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 86802eec77..0f00172a86 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -7,7 +7,7 @@ from typing import Dict, Optional, Type, Union import numpy as np -from bluesky.protocols import Descriptor, Reading, DataKey +from bluesky.protocols import DataKey, Descriptor, Reading from ophyd_async.core import ( DEFAULT_TIMEOUT, @@ -41,6 +41,7 @@ # time constant to wait for timeout A_BIT = 1e-5 + # -------------------------------------------------------------------- def ensure_proper_executor(func): @functools.wraps(func) @@ -52,6 +53,7 @@ async def wrapper(self, *args, **kwargs): return wrapper + # -------------------------------------------------------------------- def get_python_type(tango_type) -> tuple[bool, T, str]: array = is_array(tango_type) diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index b55a083b0f..2cc21645b1 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -1,6 +1,5 @@ from __future__ import annotations -from abc import abstractmethod from typing import Optional from ophyd_async.core import DEFAULT_TIMEOUT, StandardReadable @@ -18,6 +17,7 @@ class TangoReadableDevice(StandardReadable): new_device = await TangoDevice() """ + src_dict: dict = {} trl: str = "" proxy: Optional[DeviceProxy] = None diff --git a/src/ophyd_async/tango/device_controllers/new_sis3820.py b/src/ophyd_async/tango/device_controllers/new_sis3820.py deleted file mode 100644 index c678d69611..0000000000 --- a/src/ophyd_async/tango/device_controllers/new_sis3820.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations - -from typing import Dict, Optional - -from bluesky.protocols import Reading, Triggerable - -from ophyd_async.core import ( - AsyncStatus, - ConfigSignal, - HintedSignal, -) -from ophyd_async.tango import TangoReadableDevice, tango_signal_rw, tango_signal_x - - -# -------------------------------------------------------------------- -class SIS3820Counter(TangoReadableDevice, Triggerable): - - # -------------------------------------------------------------------- - def __init__(self, trl: str, name: str = "", sources: dict = None) -> None: - if sources is None: - sources = {} - self.src_dict["counts"] = sources.get("counts", "/Counts") - self.src_dict["offset"] = sources.get("offset", "/Offset") - self.src_dict["reset"] = sources.get("reset", "/Reset") - - for key in self.src_dict: - if not self.src_dict[key].startswith("/"): - self.src_dict[key] = "/" + self.src_dict[key] - - self.trl = trl - - with self.add_children_as_readables(HintedSignal): - self.counts = tango_signal_rw( - float, self.trl + self.src_dict["counts"], device_proxy=self.proxy - ) - with self.add_children_as_readables(ConfigSignal): - self.offset = tango_signal_rw( - float, self.trl + self.src_dict["offset"], device_proxy=self.proxy - ) - - self.reset = tango_signal_x( - self.trl + self.src_dict["reset"], device_proxy=self.proxy - ) - - TangoReadableDevice.__init__(self, trl, name) - self._set_success = True - - async def read(self) -> Dict[str, Reading]: - ret = await super().read() - return ret - - def trigger(self) -> AsyncStatus: - return AsyncStatus(self._trigger()) - - async def _trigger(self) -> None: - await self.reset.trigger() diff --git a/src/ophyd_async/tango/device_controllers/sis3820.py b/src/ophyd_async/tango/device_controllers/sis3820.py index 3c11eb3b67..678e63507c 100644 --- a/src/ophyd_async/tango/device_controllers/sis3820.py +++ b/src/ophyd_async/tango/device_controllers/sis3820.py @@ -4,51 +4,43 @@ from bluesky.protocols import Reading, Triggerable -from ophyd_async.core import AsyncStatus +from ophyd_async.core import ( + AsyncStatus, + ConfigSignal, + HintedSignal, +) from ophyd_async.tango import TangoReadableDevice, tango_signal_rw, tango_signal_x # -------------------------------------------------------------------- class SIS3820Counter(TangoReadableDevice, Triggerable): - src_dict: dict = { - "counts": "/counts", - "offset": "/offset", - "reset": "/reset", - } - trl: str - name: str - # -------------------------------------------------------------------- def __init__(self, trl: str, name: str = "", sources: dict = None) -> None: if sources is None: sources = {} - self.src_dict["counts"] = sources.get("counts", "/counts") - self.src_dict["offset"] = sources.get("offset", "/offset") - self.src_dict["reset"] = sources.get("reset", "/reset") + self.src_dict["counts"] = sources.get("counts", "/Counts") + self.src_dict["offset"] = sources.get("offset", "/Offset") + self.src_dict["reset"] = sources.get("reset", "/Reset") for key in self.src_dict: if not self.src_dict[key].startswith("/"): self.src_dict[key] = "/" + self.src_dict[key] - TangoReadableDevice.__init__(self, trl, name) - self._set_success = True - - # -------------------------------------------------------------------- - def register_signals(self) -> None: - self.counts = tango_signal_rw( - float, self.trl + self.src_dict["counts"], device_proxy=self.proxy - ) - self.offset = tango_signal_rw( - float, self.trl + self.src_dict["offset"], device_proxy=self.proxy - ) - - self.set_readable_signals(read_uncached=[self.counts], config=[self.offset]) + with self.add_children_as_readables(HintedSignal): + self.counts = tango_signal_rw( + float, trl + self.src_dict["counts"], device_proxy=self.proxy + ) + with self.add_children_as_readables(ConfigSignal): + self.offset = tango_signal_rw( + float, trl + self.src_dict["offset"], device_proxy=self.proxy + ) self.reset = tango_signal_x( - self.trl + self.src_dict["reset"], device_proxy=self.proxy + trl + self.src_dict["reset"], device_proxy=self.proxy ) - self.set_name(self.name) + TangoReadableDevice.__init__(self, trl, name) + self._set_success = True async def read(self) -> Dict[str, Reading]: ret = await super().read() diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 9cb890424f..2ca347abae 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -5,8 +5,8 @@ from typing import Optional, Type from ophyd_async.core import SignalR, SignalRW, SignalW, SignalX, T -from ophyd_async.tango._backend import TangoTransport from ophyd_async.core.utils import DEFAULT_TIMEOUT +from ophyd_async.tango._backend import TangoTransport from tango.asyncio import DeviceProxy __all__ = ("tango_signal_rw", "tango_signal_r", "tango_signal_w", "tango_signal_x") From 3c0c0bf17cb99bb0e634b64ffcf631ae03890b1f Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 28 Jun 2024 11:00:43 +0200 Subject: [PATCH 042/141] Rewrote SIS3820Counter as a StandardReadable to show that the TangoReadableDevice is not strictly necessary. --- .../tango/base_devices/base_device.py | 4 +-- .../tango/device_controllers/sis3820.py | 32 +++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index 2cc21645b1..d00c7ea708 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -41,13 +41,13 @@ async def closure(): await closure() await super().connect(mock=mock, timeout=timeout) - # # -------------------------------------------------------------------- + # -------------------------------------------------------------------- # @AsyncStatus.wrap # async def stage(self) -> None: # for sig in self._readables + self._configurables: # if hasattr(sig, "is_cachable") and sig.is_cachable(): # await sig.stage().task - + # # # -------------------------------------------------------------------- # @AsyncStatus.wrap # async def unstage(self) -> None: diff --git a/src/ophyd_async/tango/device_controllers/sis3820.py b/src/ophyd_async/tango/device_controllers/sis3820.py index 678e63507c..69083c9c84 100644 --- a/src/ophyd_async/tango/device_controllers/sis3820.py +++ b/src/ophyd_async/tango/device_controllers/sis3820.py @@ -5,22 +5,29 @@ from bluesky.protocols import Reading, Triggerable from ophyd_async.core import ( + DEFAULT_TIMEOUT, AsyncStatus, ConfigSignal, HintedSignal, + StandardReadable, ) -from ophyd_async.tango import TangoReadableDevice, tango_signal_rw, tango_signal_x +from ophyd_async.tango import tango_signal_rw, tango_signal_x +from tango.asyncio import DeviceProxy # -------------------------------------------------------------------- -class SIS3820Counter(TangoReadableDevice, Triggerable): +class SIS3820Counter(StandardReadable, Triggerable): # -------------------------------------------------------------------- def __init__(self, trl: str, name: str = "", sources: dict = None) -> None: if sources is None: sources = {} - self.src_dict["counts"] = sources.get("counts", "/Counts") - self.src_dict["offset"] = sources.get("offset", "/Offset") - self.src_dict["reset"] = sources.get("reset", "/Reset") + self.proxy = None + self.trl = trl + self.src_dict = { + "counts": sources.get("counts", "/Counts"), + "offset": sources.get("offset", "/Offset"), + "reset": sources.get("reset", "/Reset"), + } for key in self.src_dict: if not self.src_dict[key].startswith("/"): @@ -39,9 +46,22 @@ def __init__(self, trl: str, name: str = "", sources: dict = None) -> None: trl + self.src_dict["reset"], device_proxy=self.proxy ) - TangoReadableDevice.__init__(self, trl, name) + StandardReadable.__init__(self, name=name) self._set_success = True + async def connect( + self, + mock: bool = False, + timeout: float = DEFAULT_TIMEOUT, + force_reconnect: bool = False, + ): + async def closure(): + self.proxy = await DeviceProxy(self.trl) + return self + + await closure() + await super().connect(mock=mock, timeout=timeout) + async def read(self) -> Dict[str, Reading]: ret = await super().read() return ret From dbf6c8f6ae6c6caebd5fe53b2eb713b5a29e0026 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 28 Jun 2024 11:04:49 +0200 Subject: [PATCH 043/141] Removed unused TangoSignal frontend --- .../tango/_backend/_signal_backend.py | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 src/ophyd_async/tango/_backend/_signal_backend.py diff --git a/src/ophyd_async/tango/_backend/_signal_backend.py b/src/ophyd_async/tango/_backend/_signal_backend.py deleted file mode 100644 index be339d9fb4..0000000000 --- a/src/ophyd_async/tango/_backend/_signal_backend.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -from typing import Optional - -from ophyd_async.core import T -from ophyd_async.core.signal import _add_timeout - - -# -------------------------------------------------------------------- -# from tango attributes one can get setvalue, so we extend SignalRW and SignalW -class SignalWithSetpoint: - @_add_timeout - async def get_setpoint(self, cached: Optional[bool] = None) -> T: - """The last written value to TRL""" - return await self._backend_or_cache(cached).get_w_value() - - -# -------------------------------------------------------------------- -# not every tango attribute is configured to generate signals -class CachableOrNot: - def is_cachable(self) -> T: - """The last written value to TRL""" - return self._backend.is_cachable() - - -# # -------------------------------------------------------------------- -# class TangoSignalW(SignalW[T], CachableOrNot, SignalWithSetpoint): -# # -------------------------------------------------------------------- -# @property -# def source(self) -> Callable[[], str]: -# return self._backend.source - - -# # -------------------------------------------------------------------- -# class TangoSignalRW(SignalRW[T], CachableOrNot, SignalWithSetpoint): -# # -------------------------------------------------------------------- -# @property -# def source(self) -> Callable[[], str]: -# return self._backend.source - - -# # -------------------------------------------------------------------- -# class TangoSignalR(SignalR[T], CachableOrNot): -# # -------------------------------------------------------------------- -# @property -# def source(self) -> Callable[[], str]: -# return self._backend.source - - -# # -------------------------------------------------------------------- -# class TangoSignalX(SignalX, CachableOrNot): -# # -------------------------------------------------------------------- -# @property -# def source(self) -> Callable[[], str]: -# return self._backend.source From 1e93b4e57f2aecbafb1c68fe713cac87636eccb9 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 28 Jun 2024 11:05:03 +0200 Subject: [PATCH 044/141] minor changes --- src/ophyd_async/tango/_backend/_tango_transport.py | 2 +- src/ophyd_async/tango/base_devices/base_device.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 0f00172a86..7a8fba7831 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -437,7 +437,7 @@ async def _connect_and_store_config(self, trl): await self.proxies[trl].connect() self.trl_configs[trl] = await self.proxies[trl].get_config() except CancelledError: - raise NotConnected(self.source) + raise NotConnected(f"Could not connect to {trl}") # -------------------------------------------------------------------- async def connect(self, timeout: float = DEFAULT_TIMEOUT): diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index d00c7ea708..f7cc98b7e4 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -11,7 +11,7 @@ # -------------------------------------------------------------------- class TangoReadableDevice(StandardReadable): """ - General class for TangoDevices + General class for TangoDevices. Usage: to proper signals mount should be awaited: From 66e31889357069f41614835195e5c86635209e8b Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 28 Jun 2024 11:11:20 +0200 Subject: [PATCH 045/141] Updated TangoReadableDevice docstring --- src/ophyd_async/tango/base_devices/base_device.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index f7cc98b7e4..026aaeb3aa 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -11,11 +11,18 @@ # -------------------------------------------------------------------- class TangoReadableDevice(StandardReadable): """ - General class for TangoDevices. + General class for TangoDevices. Extends StandardReadable to provide + attributes for Tango devices. Usage: to proper signals mount should be awaited: - new_device = await TangoDevice() + + attributes: + trl: Tango resource locator, typically of the device server. + proxy: DeviceProxy object for the device. This is created when the device + is connected. + src_dict: Dictionary of the device's attributes. This can be used to account + for variation in attribute names across similar servers. """ src_dict: dict = {} From ee1166b9dc0a08543ebc7dad1160ffa106a1cc6f Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 28 Jun 2024 16:22:15 +0200 Subject: [PATCH 046/141] If events are not support, start an async polling process --- .../tango/_backend/_tango_transport.py | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 7a8fba7831..0006da0d0a 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -137,6 +137,8 @@ def unsubscribe_callback(self): class AttributeProxy(TangoProxy): support_events = False _event_callback = None + _poll_callback = None + _poll_task = None _eid = None # -------------------------------------------------------------------- @@ -201,15 +203,20 @@ def has_subscription(self) -> bool: # -------------------------------------------------------------------- def subscribe_callback(self, callback: Optional[ReadingValueCallback]): - """add user callback to CHANGE event subscription""" - self._event_callback = callback - if not self._eid: - self._eid = self._proxy.subscribe_event( - self._name, - EventType.CHANGE_EVENT, - self._event_processor, - green_mode=False, - ) + # If the attribute supports events, then we can subscribe to them + if self.support_events: + """add user callback to CHANGE event subscription""" + self._event_callback = callback + if not self._eid: + self._eid = self._proxy.subscribe_event( + self._name, + EventType.CHANGE_EVENT, + self._event_processor, + green_mode=False, + ) + else: + self._poll_callback = callback + self._poll_task = asyncio.create_task(self.poll()) # -------------------------------------------------------------------- def unsubscribe_callback(self): @@ -217,6 +224,7 @@ def unsubscribe_callback(self): self._proxy.unsubscribe_event(self._eid, green_mode=False) self._eid = None self._event_callback = None + self._poll_callback = None # -------------------------------------------------------------------- def _event_processor(self, event): @@ -230,6 +238,17 @@ def _event_processor(self, event): self._event_callback(reading, value) + # -------------------------------------------------------------------- + async def poll(self): + if self._poll_callback: + while True: + await asyncio.sleep(0.5) + reading = await self.get_reading() + if self._poll_callback: + self._poll_callback(reading, reading["value"]) + else: + break + # -------------------------------------------------------------------- class CommandProxy(TangoProxy): @@ -480,9 +499,9 @@ def is_cachable(self): # -------------------------------------------------------------------- def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: - assert self.proxies[ - self.read_trl - ].support_events, f"{self.source} does not support events" + # assert self.proxies[ + # self.read_trl + # ].support_events, f"{self.source} does not support events" if callback: assert not self.proxies[ From f2c5f433e84436dfd252e4312ef8b2d4fa5d7268 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 2 Jul 2024 15:10:21 +0200 Subject: [PATCH 047/141] Improved polling. Added methods to TangoTransport to allow events and to configure/allow polling. If a callback is set when neither events or polling are allowed, an error is raised. --- .../tango/_backend/_tango_transport.py | 132 ++++++++++++++++-- 1 file changed, 120 insertions(+), 12 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 0006da0d0a..6e15409b5e 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -132,6 +132,17 @@ def subscribe_callback(self, callback: Optional[ReadingValueCallback]): def unsubscribe_callback(self): """delete CHANGE event subscription""" + # -------------------------------------------------------------------- + @abstractmethod + def set_polling( + self, + allow_polling: bool = True, + polling_period: float = 0.1, + abs_change=None, + rel_change=None, + ): + """Set polling parameters""" + # -------------------------------------------------------------------- class AttributeProxy(TangoProxy): @@ -140,6 +151,10 @@ class AttributeProxy(TangoProxy): _poll_callback = None _poll_task = None _eid = None + _abs_change = None + _rel_change = 0.1 + _polling_period = 0.5 + _allow_polling = False # -------------------------------------------------------------------- async def connect(self) -> None: @@ -215,8 +230,10 @@ def subscribe_callback(self, callback: Optional[ReadingValueCallback]): green_mode=False, ) else: - self._poll_callback = callback - self._poll_task = asyncio.create_task(self.poll()) + if self._allow_polling: + self._poll_callback = callback + if self._poll_callback is not None: + self._poll_task = asyncio.create_task(self.poll()) # -------------------------------------------------------------------- def unsubscribe_callback(self): @@ -240,14 +257,66 @@ def _event_processor(self, event): # -------------------------------------------------------------------- async def poll(self): - if self._poll_callback: - while True: - await asyncio.sleep(0.5) - reading = await self.get_reading() - if self._poll_callback: + """ + Poll the attribute and call the callback if the value has changed by more + than the absolute or relative change. This function is used when an attribute + that does not support events is cached or a callback is passed to it. + """ + + last_reading = await self.get_reading() + flag = 0 + + while True: + await asyncio.sleep(self._polling_period) + + reading = await self.get_reading() + + if reading is None: + continue + if reading["value"] is None: + continue + + diff = abs(reading["value"] - last_reading["value"]) + + if self._abs_change is not None: + if diff > self._abs_change: + if self._poll_callback is not None: + self._poll_callback(reading, reading["value"]) + flag = 0 + + elif self._rel_change is not None: + if last_reading["value"] is not None: + if diff > self._rel_change * abs(last_reading["value"]): + if self._poll_callback is not None: + self._poll_callback(reading, reading["value"]) + flag = 0 + + else: + flag += 1 + if flag >= 4 and self._poll_callback is not None: self._poll_callback(reading, reading["value"]) - else: - break + flag = 0 + + last_reading = reading.copy() + if self._poll_callback is None: + "Polling stopped. No callback set." + break + + # -------------------------------------------------------------------- + def set_polling( + self, + allow_polling: bool = False, + polling_period: float = 0.5, + abs_change=None, + rel_change=0.1, + ): + """ + Set the polling parameters. + """ + self._allow_polling = allow_polling + self._polling_period = polling_period + self._abs_change = abs_change + self._rel_change = rel_change # -------------------------------------------------------------------- @@ -293,6 +362,16 @@ async def get_config(self) -> CommandInfo: async def get_reading(self) -> Reading: return self._last_reading + # -------------------------------------------------------------------- + def set_polling( + self, + allow_polling: bool = False, + polling_period: float = 0.5, + abs_change=None, + rel_change=0.1, + ): + pass + # -------------------------------------------------------------------- def get_dtype_extended(datatype): @@ -444,6 +523,8 @@ def __init__( } self.trl_configs: Dict[str, AttributeInfoEx] = {} self.descriptor: Descriptor = {} # type: ignore + self.polling = (False, 0.5, None, 0.1) + self.support_events = False # -------------------------------------------------------------------- def source(self, name: str) -> str: @@ -455,6 +536,7 @@ async def _connect_and_store_config(self, trl): self.proxies[trl] = await get_tango_trl(trl, self.proxies[trl]) await self.proxies[trl].connect() self.trl_configs[trl] = await self.proxies[trl].get_config() + self.proxies[trl].support_events = self.support_events except CancelledError: raise NotConnected(f"Could not connect to {trl}") @@ -469,6 +551,7 @@ async def connect(self, timeout: float = DEFAULT_TIMEOUT): else: # The same, so only need to connect one await self._connect_and_store_config(self.read_trl) + self.proxies[self.read_trl].set_polling(*self.polling) self.descriptor = get_trl_descriptor( self.datatype, self.read_trl, self.trl_configs ) @@ -499,9 +582,15 @@ def is_cachable(self): # -------------------------------------------------------------------- def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: - # assert self.proxies[ - # self.read_trl - # ].support_events, f"{self.source} does not support events" + if ( + self.proxies[self.read_trl].support_events is False + and self.polling[0] is False + ): + raise RuntimeError( + f"Cannot set event for {self.read_trl}. " + "Use set_polling to enable polling for this signal or use signal as" + " non-cached." + ) if callback: assert not self.proxies[ @@ -519,3 +608,22 @@ def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: self.proxies[self.read_trl].unsubscribe_callback() # -------------------------------------------------------------------- + + def set_polling( + self, + allow_polling: bool = False, + polling_period: float = 0.5, + abs_change=None, + rel_change=0.1, + ): + self.polling = (allow_polling, polling_period, abs_change, rel_change) + if self.proxies[self.read_trl] is not None: + self.proxies[self.read_trl].set_polling( + allow_polling, polling_period, abs_change, rel_change + ) + + # -------------------------------------------------------------------- + def allow_events(self, allow: bool = True): + self.support_events = allow + if self.proxies[self.read_trl] is not None: + self.proxies[self.read_trl].support_events = self.support_events From d4379c44e2cc376fa75019e31b1c5def3e395c7a Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 3 Jul 2024 09:57:37 +0200 Subject: [PATCH 048/141] Simplified callback attributes --- .../tango/_backend/_tango_transport.py | 64 +++++++++---------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 6e15409b5e..2820e77de3 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -146,11 +146,10 @@ def set_polling( # -------------------------------------------------------------------- class AttributeProxy(TangoProxy): + _callback = None support_events = False - _event_callback = None - _poll_callback = None - _poll_task = None _eid = None + _poll_task = None _abs_change = None _rel_change = 0.1 _polling_period = 0.5 @@ -214,14 +213,14 @@ async def get_reading(self) -> Reading: # -------------------------------------------------------------------- def has_subscription(self) -> bool: - return bool(self._eid) + return bool(self._callback) # -------------------------------------------------------------------- def subscribe_callback(self, callback: Optional[ReadingValueCallback]): # If the attribute supports events, then we can subscribe to them + self._callback = callback if self.support_events: """add user callback to CHANGE event subscription""" - self._event_callback = callback if not self._eid: self._eid = self._proxy.subscribe_event( self._name, @@ -229,19 +228,27 @@ def subscribe_callback(self, callback: Optional[ReadingValueCallback]): self._event_processor, green_mode=False, ) + elif self._allow_polling: + """start polling if no events supported""" + if self._callback is not None: + self._poll_task = asyncio.create_task(self.poll()) else: - if self._allow_polling: - self._poll_callback = callback - if self._poll_callback is not None: - self._poll_task = asyncio.create_task(self.poll()) + self.unsubscribe_callback() + raise RuntimeError( + f"Cannot set event for {self._name}. " + "Cannot set a callback on an attribute that does not support events and" + " for which polling is disabled." + ) # -------------------------------------------------------------------- def unsubscribe_callback(self): if self._eid: self._proxy.unsubscribe_event(self._eid, green_mode=False) self._eid = None - self._event_callback = None - self._poll_callback = None + if self._poll_task: + self._poll_task.cancel() + self._poll_task = None + self._callback = None # -------------------------------------------------------------------- def _event_processor(self, event): @@ -252,8 +259,7 @@ def _event_processor(self, event): "timestamp": event.get_date().totime(), "alarm_severity": event.attr_value.quality, } - - self._event_callback(reading, value) + self._callback(reading, value) # -------------------------------------------------------------------- async def poll(self): @@ -262,44 +268,36 @@ async def poll(self): than the absolute or relative change. This function is used when an attribute that does not support events is cached or a callback is passed to it. """ - last_reading = await self.get_reading() flag = 0 while True: await asyncio.sleep(self._polling_period) - reading = await self.get_reading() - - if reading is None: + if reading is None or reading["value"] is None: continue - if reading["value"] is None: - continue - diff = abs(reading["value"] - last_reading["value"]) if self._abs_change is not None: if diff > self._abs_change: - if self._poll_callback is not None: - self._poll_callback(reading, reading["value"]) + if self._callback is not None: + self._callback(reading, reading["value"]) flag = 0 elif self._rel_change is not None: if last_reading["value"] is not None: if diff > self._rel_change * abs(last_reading["value"]): - if self._poll_callback is not None: - self._poll_callback(reading, reading["value"]) + if self._callback is not None: + self._callback(reading, reading["value"]) flag = 0 else: - flag += 1 - if flag >= 4 and self._poll_callback is not None: - self._poll_callback(reading, reading["value"]) - flag = 0 + flag = (flag + 1) % 4 + if flag == 0 and self._callback is not None: + self._callback(reading, reading["value"]) last_reading = reading.copy() - if self._poll_callback is None: - "Polling stopped. No callback set." + if self._callback is None: break # -------------------------------------------------------------------- @@ -523,7 +521,7 @@ def __init__( } self.trl_configs: Dict[str, AttributeInfoEx] = {} self.descriptor: Descriptor = {} # type: ignore - self.polling = (False, 0.5, None, 0.1) + self.polling = (True, 0.5, None, 0.1) self.support_events = False # -------------------------------------------------------------------- @@ -588,8 +586,8 @@ def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: ): raise RuntimeError( f"Cannot set event for {self.read_trl}. " - "Use set_polling to enable polling for this signal or use signal as" - " non-cached." + "Cannot set a callback on an attribute that does not support events and" + " for which polling is disabled." ) if callback: From f8a017b14ce02c78326bc0bdf279f5299ce3335b Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 3 Jul 2024 10:43:09 +0200 Subject: [PATCH 049/141] Refactor to use WatchableAsyncStatus --- .../tango/device_controllers/omsvme58.py | 197 +++++++++--------- 1 file changed, 97 insertions(+), 100 deletions(-) diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index 8ed2ba8c2f..0b950cd23f 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -1,12 +1,24 @@ from __future__ import annotations import asyncio -import time -from typing import Callable, List, Optional +from asyncio import Event +from typing import Optional -from bluesky.protocols import Locatable, Location, Stoppable +from bluesky.protocols import Movable, Stoppable -from ophyd_async.core import AsyncStatus +from ophyd_async.core import ( + AsyncStatus, + ConfigSignal, + HintedSignal, + WatchableAsyncStatus, +) +from ophyd_async.core.signal import observe_value +from ophyd_async.core.utils import ( + DEFAULT_TIMEOUT, + CalculatableTimeout, + CalculateTimeout, + WatcherUpdate, +) from ophyd_async.tango import ( TangoReadableDevice, tango_signal_r, @@ -17,127 +29,112 @@ # -------------------------------------------------------------------- -class OmsVME58Motor(TangoReadableDevice, Locatable, Stoppable): - trl: str - name: str - src_dict: dict = { - "position": "/position", - "baserate": "/baserate", - "slewrate": "/slewrate", - "conversion": "/conversion", - "acceleration": "/acceleration", - "stop": "/stopmove", - "state": "/state", - } - +class OmsVME58Motor(TangoReadableDevice, Movable, Stoppable): # -------------------------------------------------------------------- def __init__(self, trl: str, name: str = "", sources: dict = None) -> None: if sources is None: sources = {} - self.src_dict["position"] = sources.get("position", "/position") - self.src_dict["baserate"] = sources.get("baserate", "/baserate") - self.src_dict["slewrate"] = sources.get("slewrate", "/slewrate") - self.src_dict["conversion"] = sources.get("conversion", "/conversion") - self.src_dict["acceleration"] = sources.get("acceleration", "/acceleration") - self.src_dict["stop"] = sources.get("stop", "/stopmove") - self.src_dict["state"] = sources.get("state", "/state") + self.src_dict["position"] = sources.get("position", "/Position") + self.src_dict["baserate"] = sources.get("baserate", "/BaseRate") + self.src_dict["slewrate"] = sources.get("slewrate", "/SlewRate") + self.src_dict["conversion"] = sources.get("conversion", "/Conversion") + self.src_dict["acceleration"] = sources.get("acceleration", "/Acceleration") + self.src_dict["stop"] = sources.get("stop", "/StopMove") + self.src_dict["state"] = sources.get("state", "/State") for key in self.src_dict: if not self.src_dict[key].startswith("/"): self.src_dict[key] = "/" + self.src_dict[key] + with self.add_children_as_readables(HintedSignal): + self.position = tango_signal_rw( + float, trl + self.src_dict["position"], device_proxy=self.proxy + ) + with self.add_children_as_readables(ConfigSignal): + self.baserate = tango_signal_rw( + int, trl + self.src_dict["baserate"], device_proxy=self.proxy + ) + self.slewrate = tango_signal_rw( + int, trl + self.src_dict["slewrate"], device_proxy=self.proxy + ) + self.conversion = tango_signal_rw( + float, trl + self.src_dict["conversion"], device_proxy=self.proxy + ) + self.acceleration = tango_signal_rw( + int, trl + self.src_dict["acceleration"], device_proxy=self.proxy + ) + + self._stop = tango_signal_x(trl + self.src_dict["stop"], self.proxy) + self._state = tango_signal_r(DevState, trl + self.src_dict["state"], self.proxy) + TangoReadableDevice.__init__(self, trl, name) self._set_success = True - # -------------------------------------------------------------------- - def register_signals(self) -> None: - self.position = tango_signal_rw( - float, self.trl + self.src_dict["position"], device_proxy=self.proxy - ) - self.baserate = tango_signal_rw( - int, self.trl + self.src_dict["baserate"], device_proxy=self.proxy - ) - self.slewrate = tango_signal_rw( - int, self.trl + self.src_dict["slewrate"], device_proxy=self.proxy - ) - self.conversion = tango_signal_rw( - float, self.trl + self.src_dict["conversion"], device_proxy=self.proxy - ) - self.acceleration = tango_signal_rw( - int, self.trl + self.src_dict["acceleration"], device_proxy=self.proxy - ) - - self.set_readable_signals( - read_uncached=[self.position], - config=[self.baserate, self.slewrate, self.conversion, self.acceleration], + @WatchableAsyncStatus.wrap + async def set( + self, + new_position: float, + timeout: CalculatableTimeout = CalculateTimeout, + ): + self._set_success = True + ( + old_position, + conversion, + velocity, + acceleration, + ) = await asyncio.gather( + self.position.get_value(), + self.conversion.get_value(), + self.slewrate.get_value(), + self.acceleration.get_value(), ) - self._stop = tango_signal_x(self.trl + self.src_dict["stop"], self.proxy) - self._state = tango_signal_r( - DevState, self.trl + self.src_dict["state"], self.proxy - ) + if timeout is CalculateTimeout: + assert velocity > 0, "Motor has zero velocity" + timeout = ( + (abs(new_position - old_position) * conversion / velocity) + + (2 * velocity / acceleration) + + DEFAULT_TIMEOUT + ) - self.set_name(self.name) + await self.position.set(new_position, wait=True, timeout=timeout) - # -------------------------------------------------------------------- - async def _move( - self, new_position: float, watchers: List[Callable] or None = None - ) -> None: - if watchers is None: - watchers = [] - self._set_success = True - start = time.monotonic() - start_position = await self.position.get_value() + move_status = AsyncStatus(self._wait()) - def update_watchers(current_position: float) -> None: - for watcher in watchers: - watcher( - name=self.name, + try: + async for current_position in observe_value( + self.position, done_status=move_status + ): + yield WatcherUpdate( current=current_position, - initial=start_position, + initial=old_position, target=new_position, - time_elapsed=time.monotonic() - start, + name=self.name, ) + except RuntimeError as exc: + print(f"RuntimeError: {exc}") + raise - if self.position.is_cachable(): - self.position.subscribe_value(update_watchers) - else: - update_watchers(start_position) - try: - await self.position.set(new_position) - await asyncio.sleep(0.1) - counter = 0 - state = await self._state.get_value() - while state == DevState.MOVING: - # Update the watchers with the current position every 0.5 seconds - if counter % 5 == 0: - current_position = await self.position.get_value() - update_watchers(current_position) - counter = 0 - await asyncio.sleep(0.1) - state = await self._state.get_value() - counter += 1 - finally: - if self.position.is_cachable(): - self.position.clear_sub(update_watchers) - else: - update_watchers(await self.position.get_value()) if not self._set_success: raise RuntimeError("Motor was stopped") - # -------------------------------------------------------------------- - def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus: - watchers: List[Callable] = [] - coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout) - return AsyncStatus(coro) - - # -------------------------------------------------------------------- - async def locate(self) -> Location: - set_point = await self.position.get_setpoint() - readback = await self.position.get_value() - return Location(setpoint=set_point, readback=readback) - # -------------------------------------------------------------------- def stop(self, success: bool = False) -> AsyncStatus: self._set_success = success return self._stop.trigger() + + # -------------------------------------------------------------------- + async def _wait(self, event: Optional[Event] = None) -> None: + await asyncio.sleep(0.5) + state = await self._state.get_value() + try: + while state == DevState.MOVING: + await asyncio.sleep(0.1) + state = await self._state.get_value() + except Exception as e: + raise RuntimeError(f"Error waiting for motor to stop: {e}") + finally: + if event: + event.set() + if state != DevState.ON: + raise RuntimeError(f"Motor did not stop correctly. State {state}") From 2abc525900dcbccc487075b9e9fdbe5f0ccbce93 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 4 Jul 2024 10:36:52 +0200 Subject: [PATCH 050/141] SignalCache needs an initial reading --- src/ophyd_async/tango/_backend/_tango_transport.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 2820e77de3..c0ff284ee4 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -270,6 +270,9 @@ async def poll(self): """ last_reading = await self.get_reading() flag = 0 + # Initial reading + if self._callback is not None: + self._callback(last_reading, last_reading["value"]) while True: await asyncio.sleep(self._polling_period) From c5a1a74541b7dbaf6b9a61be7e70b11b4bc0ac7b Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 4 Jul 2024 10:41:22 +0200 Subject: [PATCH 051/141] Updated motor to use cached signals now that polling is enabled by default on the tango backend --- src/ophyd_async/tango/device_controllers/omsvme58.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py index 0b950cd23f..563141bd49 100644 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ b/src/ophyd_async/tango/device_controllers/omsvme58.py @@ -88,7 +88,6 @@ async def set( self.slewrate.get_value(), self.acceleration.get_value(), ) - if timeout is CalculateTimeout: assert velocity > 0, "Motor has zero velocity" timeout = ( @@ -114,7 +113,6 @@ async def set( except RuntimeError as exc: print(f"RuntimeError: {exc}") raise - if not self._set_success: raise RuntimeError("Motor was stopped") From 0aab38ace5911df43590e626c89a0bb007a70eb0 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 4 Jul 2024 10:49:48 +0200 Subject: [PATCH 052/141] Removed is_cachable. Passing None to allow_events will return the current state of the proxy.support_events attribute --- src/ophyd_async/tango/_backend/_tango_transport.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index c0ff284ee4..33a3dc8f47 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -577,10 +577,6 @@ async def get_value(self) -> T: async def get_setpoint(self) -> T: return await self.proxies[self.write_trl].get_w_value() - # -------------------------------------------------------------------- - def is_cachable(self): - return self.proxies[self.read_trl].support_events - # -------------------------------------------------------------------- def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: if ( @@ -624,7 +620,8 @@ def set_polling( ) # -------------------------------------------------------------------- - def allow_events(self, allow: bool = True): + def allow_events(self, allow: Optional[bool] = True): self.support_events = allow if self.proxies[self.read_trl] is not None: self.proxies[self.read_trl].support_events = self.support_events + return self.proxies[self.read_trl].support_events From 7583ce90331ca12297e2f0c0173d0ca383e96a77 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 4 Jul 2024 13:07:37 +0200 Subject: [PATCH 053/141] updated timer example class --- .../tango/device_controllers/dgg2.py | 177 ++++++++++-------- 1 file changed, 94 insertions(+), 83 deletions(-) diff --git a/src/ophyd_async/tango/device_controllers/dgg2.py b/src/ophyd_async/tango/device_controllers/dgg2.py index 9e4e9d433f..f825692d8a 100644 --- a/src/ophyd_async/tango/device_controllers/dgg2.py +++ b/src/ophyd_async/tango/device_controllers/dgg2.py @@ -1,12 +1,17 @@ from __future__ import annotations import asyncio -import time -from typing import Callable, List +from asyncio import Event +from typing import Optional from bluesky.protocols import Preparable, Triggerable -from ophyd_async.core import AsyncStatus +from ophyd_async.core import ( + AsyncStatus, + ConfigSignal, + HintedSignal, +) +from ophyd_async.core.utils import DEFAULT_TIMEOUT from ophyd_async.tango import ( TangoReadableDevice, tango_signal_r, @@ -18,47 +23,35 @@ # -------------------------------------------------------------------- class DGG2Timer(TangoReadableDevice, Triggerable, Preparable): - trl: str - name: str - src_dict: dict = { - "sampletime": "/sampletime", - "remainingtime": "/remainingtime", - "startandwaitfortimer": "/startandwaitfortimer", - "start": "/start", - "state": "/state", - } - # -------------------------------------------------------------------- def __init__(self, trl: str, name="", sources: dict = None) -> None: if sources is None: sources = {} - self.src_dict["sampletime"] = sources.get("sampletime", "/sampletime") - self.src_dict["remainingtime"] = sources.get("remainingtime", "/remainingtime") + self.trl = trl + self.src_dict["sampletime"] = sources.get("sampletime", "/SampleTime") + self.src_dict["remainingtime"] = sources.get("remainingtime", "/RemainingTime") self.src_dict["startandwaitfortimer"] = sources.get( - "startandwaitfortimer", "/startandwaitfortimer" + "startandwaitfortimer", "/StartAndWaitForTimer" ) - self.src_dict["start"] = sources.get("start", "/start") - self.src_dict["state"] = sources.get("state", "/state") + self.src_dict["start"] = sources.get("start", "/Start") + self.src_dict["state"] = sources.get("state", "/State") for key in self.src_dict: if not self.src_dict[key].startswith("/"): self.src_dict[key] = "/" + self.src_dict[key] - TangoReadableDevice.__init__(self, trl, name) - self._set_success = True + # Add sampletime as an unchached hinted signal + with self.add_children_as_readables(ConfigSignal): + self.sampletime = tango_signal_rw( + float, self.trl + self.src_dict["sampletime"], device_proxy=self.proxy + ) - # -------------------------------------------------------------------- - def register_signals(self) -> None: - self.sampletime = tango_signal_rw( - float, self.trl + self.src_dict["sampletime"], device_proxy=self.proxy - ) - self.remainingtime = tango_signal_rw( - float, self.trl + self.src_dict["remainingtime"], device_proxy=self.proxy - ) - - self.set_readable_signals( - read_uncached=[self.sampletime], config=[self.sampletime] - ) + with self.add_children_as_readables(HintedSignal): + self.remainingtime = tango_signal_rw( + float, + self.trl + self.src_dict["remainingtime"], + device_proxy=self.proxy, + ) self.startandwaitfortimer = tango_signal_x( self.trl + self.src_dict["startandwaitfortimer"], device_proxy=self.proxy @@ -72,61 +65,79 @@ def register_signals(self) -> None: DevState, self.trl + self.src_dict["state"], self.proxy ) - self.set_name(self.name) - - # -------------------------------------------------------------------- - async def _trigger(self, watchers: List[Callable] or None = None) -> None: - if watchers is None: - watchers = [] + TangoReadableDevice.__init__(self, trl, name) self._set_success = True - start = time.monotonic() - total_time = await self.sampletime.get_value() - - def update_watchers(remaining_time: float) -> None: - for watcher in watchers: - watcher( - name=self.name, - current=remaining_time, - initial=total_time, - target=total_time, - time_elapsed=time.monotonic() - start, - ) - - if self.remainingtime.is_cachable(): - self.remainingtime.subscribe_value(update_watchers) - else: - update_watchers(total_time) - try: - await self.start.trigger() - finally: - if self.remainingtime.is_cachable(): - self.remainingtime.clear_sub(update_watchers) - else: - counter = 0 - state = await self._state.get_value() - while state == DevState.MOVING: - # Update the watchers with the current position every 0.5 seconds - if counter % 5 == 0: - remaining_time = await self.remainingtime.get_value() - update_watchers(remaining_time) - counter = 0 - await asyncio.sleep(0.1) - state = await self._state.get_value() - counter += 1 - - # update_watchers(await self.remainingtime.get_value()) - if not self._set_success: - raise RuntimeError("Timer was not triggered") - # -------------------------------------------------------------------- - def trigger(self) -> AsyncStatus: - watchers: List[Callable] = [] - return AsyncStatus(self._trigger(watchers)) + # # -------------------------------------------------------------------- + # async def _trigger(self, watchers: List[Callable] or None = None) -> None: + # if watchers is None: + # watchers = [] + # self._set_success = True + # start = time.monotonic() + # total_time = await self.sampletime.get_value() + # + # def update_watchers(remaining_time: float) -> None: + # for watcher in watchers: + # watcher( + # name=self.name, + # current=remaining_time, + # initial=total_time, + # target=total_time, + # time_elapsed=time.monotonic() - start, + # ) + # + # if self.remainingtime._cache is False: + # self.remainingtime.subscribe_value(update_watchers) + # else: + # update_watchers(total_time) + # try: + # await self.start.trigger() + # if self.remainingtime._cache is None: + # counter = 0 + # state = await self._state.get_value() + # while state == DevState.MOVING: + # # Update the watchers with the current position every 0.5 seconds + # if counter % 5 == 0: + # remaining_time = await self.remainingtime.get_value() + # update_watchers(remaining_time) + # counter = 0 + # await asyncio.sleep(0.1) + # state = await self._state.get_value() + # counter += 1 + # except Exception as e: + # raise RuntimeError("Failed to trigger timer") from e + # if not self._set_success: + # raise RuntimeError("Timer was not triggered") + # + # # -------------------------------------------------------------------- + # def trigger(self) -> AsyncStatus: + # watchers: List[Callable] = [] + # return AsyncStatus(self._trigger(watchers)) # -------------------------------------------------------------------- def prepare(self, p_time: float) -> AsyncStatus: return self.sampletime.set(p_time) - # -------------------------------------------------------------------- - async def set_time(self, s_time: float) -> None: - await self.sampletime.set(s_time) + def trigger(self): + return AsyncStatus(self._trigger()) + + async def _trigger(self): + sample_time = await self.sampletime.get_value() + timeout = sample_time + DEFAULT_TIMEOUT + await self.start.trigger(wait=True, timeout=timeout) + await self._wait() + + async def _wait(self, event: Optional[Event] = None) -> None: + # await asyncio.sleep(0.5) + state = await self._state.get_value() + try: + while state == DevState.MOVING: + await asyncio.sleep(0.1) + state = await self._state.get_value() + except Exception as e: + raise RuntimeError(f"Error waiting for motor to stop: {e}") + finally: + if event: + event.set() + if state != DevState.ON: + raise RuntimeError(f"Motor did not stop correctly. State {state}") From 810fad0cfc66bd2d47efc210bfe592144d825648 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 4 Jul 2024 13:09:22 +0200 Subject: [PATCH 054/141] Removed tango_mover --- .../tango/device_controllers/tango_mover.py | 230 ------------------ 1 file changed, 230 deletions(-) delete mode 100644 src/ophyd_async/tango/device_controllers/tango_mover.py diff --git a/src/ophyd_async/tango/device_controllers/tango_mover.py b/src/ophyd_async/tango/device_controllers/tango_mover.py deleted file mode 100644 index 36add7627a..0000000000 --- a/src/ophyd_async/tango/device_controllers/tango_mover.py +++ /dev/null @@ -1,230 +0,0 @@ -from __future__ import annotations - -import asyncio -import time -from typing import Callable, List, Optional - -from bluesky.protocols import Locatable, Location, Stoppable - -from ophyd_async.core import AsyncStatus -from ophyd_async.tango import ( - TangoReadableDevice, - tango_signal_r, - tango_signal_rw, - tango_signal_w, - tango_signal_x, -) -from tango import DevState - - -# -------------------------------------------------------------------- -class TangoMover(TangoReadableDevice, Locatable, Stoppable): - # -------------------------------------------------------------------- - def __init__(self, trl: str, name="", signals: dict = None) -> None: - """ - Generic moveable tango device. Can be used in combination with an appropriately - formatted signal dictionary to instantiate any tango device that can be driven - along a single axis. - - signals must be a dictionary that contains at least the following keys. Each key - will be a signal of the same name on the device. - - position - - _state - - _stop - - Values must be dictionaries of the following form - { - "dtype": "float", - "source": "/position", - "sig_type": "rw" - "readable": True - "configurable": True - "read_uncached": False - } - - Parameters - ---------- - trl : str - The Tango device trl - name : str - The name of the device - signals : dict - A dictionary containing the signals for the device - """ - # Check that the signals dictionary is valid - if not signals: - raise ValueError("Signals dictionary must be provided") - if "position" not in signals: - raise ValueError("Signals dictionary must contain a position signal") - if "_state" not in signals: - raise ValueError("Signals dictionary must contain a _state signal") - if "_stop" not in signals: - raise ValueError("Signals dictionary must contain a _stop signal") - - self.position = None - self._state = None - self._stop = None - self._signal_list = signals - TangoReadableDevice.__init__(self, trl, name) - self._set_success = True - - # -------------------------------------------------------------------- - def _add_signal(self, **kwargs: str) -> None: - """ - Add a signal to the device - - Required Parameters - ---------- - dtype : The data type of the value of the signal - name : The name given to the signal object - source : The source of the signal, appended to the device trl - sig_type : The type of signal (r, w, rw, or x) - """ - required_keys = ["dtype", "name", "source", "sig_type"] - for key in required_keys: - if key not in kwargs: - raise ValueError(f"Missing required key {key}") - dtype = kwargs["dtype"] - name = kwargs["name"] - source = kwargs["source"] - sig_type = kwargs["sig_type"] - - # Convert the data type string to the actual type - if dtype.lower() == "int": - f_dtype = int - elif dtype.lower() == "float": - f_dtype = float - elif dtype.lower() == "str": - f_dtype = str - elif dtype == "DevState": - f_dtype = DevState - elif dtype.lower() == "none": - f_dtype = None - else: - raise ValueError(f"Invalid data type {dtype}") - - if sig_type == "r": - setattr( - self, - name, - tango_signal_r(f_dtype, self.trl + source, device_proxy=self.proxy), - ) - elif sig_type == "w": - setattr( - self, - name, - tango_signal_w(f_dtype, self.trl + source, device_proxy=self.proxy), - ) - elif sig_type == "rw": - setattr( - self, - name, - tango_signal_rw(f_dtype, self.trl + source, device_proxy=self.proxy), - ) - elif sig_type == "x": - setattr( - self, name, tango_signal_x(self.trl + source, device_proxy=self.proxy) - ) - else: - raise ValueError(f"Invalid signal type {sig_type}") - - # -------------------------------------------------------------------- - def register_signals(self) -> None: - # If signal names are not prepended by /, add it - for name in self._signal_list: - if not self._signal_list[name]["source"].startswith("/"): - self._signal_list[name]["source"] = ( - "/" + self._signal_list[name]["source"] - ) - - # If the dtype of the _state signal is not DevState, change it - if self._signal_list["_state"]["dtype"] != "DevState": - self._signal_list["_state"]["dtype"] = "DevState" - - # Parse the signal list and add the signals to the device - for name, signal in self._signal_list.items(): - self._add_signal(name=name, **signal) - - # Set the readable signals - self.set_readable_signals( - read=[ - getattr(self, name) - for name in self._signal_list - if self._signal_list[name]["readable"] - ], - config=[ - getattr(self, name) - for name in self._signal_list - if self._signal_list[name]["configurable"] - ], - read_uncached=[ - getattr(self, name) - for name in self._signal_list - if self._signal_list[name]["read_uncached"] - ], - ) - - self.set_name(self.name) - - # -------------------------------------------------------------------- - async def _move( - self, new_position: float, watchers: List[Callable] or None = None - ) -> None: - if watchers is None: - watchers = [] - self._set_success = True - start = time.monotonic() - start_position = await self.position.get_value() - - def update_watchers(current_position: float) -> None: - for watcher in watchers: - watcher( - name=self.name, - current=current_position, - initial=start_position, - target=new_position, - time_elapsed=time.monotonic() - start, - ) - - if self.position.is_cachable(): - self.position.subscribe_value(update_watchers) - else: - update_watchers(start_position) - try: - await self.position.set(new_position) - await asyncio.sleep(0.1) - counter = 0 - while await self._state.get_value() == DevState.MOVING: - # Update the watchers with the current position every 0.5 seconds - if counter % 5 == 0: - current_position = await self.position.get_value() - update_watchers(current_position) - counter = 0 - await asyncio.sleep(0.1) - counter += 1 - finally: - if self.position.is_cachable(): - self.position.clear_sub(update_watchers) - else: - update_watchers(await self.position.get_value()) - if not self._set_success: - raise RuntimeError("Motor was stopped") - - # -------------------------------------------------------------------- - def set(self, new_position: float, timeout: Optional[float] = None) -> AsyncStatus: - watchers: List[Callable] = [] - coro = asyncio.wait_for(self._move(new_position, watchers), timeout=timeout) - return AsyncStatus(coro) - - # -------------------------------------------------------------------- - async def locate(self) -> Location: - set_point = await self.position.get_setpoint() - readback = await self.position.get_value() - return Location(setpoint=set_point, readback=readback) - - # -------------------------------------------------------------------- - async def stop(self, success: bool = True) -> None: - self._set_success = not success - # Put with completion will never complete as we are waiting for completion on - # the move above, so need to pass wait=False - await self._stop.trigger() From 5f502cea4746f07b894aa13ae52e115b5d768e07 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 4 Jul 2024 13:27:09 +0200 Subject: [PATCH 055/141] Removed commented code --- .../tango/device_controllers/dgg2.py | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/src/ophyd_async/tango/device_controllers/dgg2.py b/src/ophyd_async/tango/device_controllers/dgg2.py index f825692d8a..1615a29e80 100644 --- a/src/ophyd_async/tango/device_controllers/dgg2.py +++ b/src/ophyd_async/tango/device_controllers/dgg2.py @@ -68,53 +68,6 @@ def __init__(self, trl: str, name="", sources: dict = None) -> None: TangoReadableDevice.__init__(self, trl, name) self._set_success = True - # # -------------------------------------------------------------------- - # async def _trigger(self, watchers: List[Callable] or None = None) -> None: - # if watchers is None: - # watchers = [] - # self._set_success = True - # start = time.monotonic() - # total_time = await self.sampletime.get_value() - # - # def update_watchers(remaining_time: float) -> None: - # for watcher in watchers: - # watcher( - # name=self.name, - # current=remaining_time, - # initial=total_time, - # target=total_time, - # time_elapsed=time.monotonic() - start, - # ) - # - # if self.remainingtime._cache is False: - # self.remainingtime.subscribe_value(update_watchers) - # else: - # update_watchers(total_time) - # try: - # await self.start.trigger() - # if self.remainingtime._cache is None: - # counter = 0 - # state = await self._state.get_value() - # while state == DevState.MOVING: - # # Update the watchers with the current position every 0.5 seconds - # if counter % 5 == 0: - # remaining_time = await self.remainingtime.get_value() - # update_watchers(remaining_time) - # counter = 0 - # await asyncio.sleep(0.1) - # state = await self._state.get_value() - # counter += 1 - # except Exception as e: - # raise RuntimeError("Failed to trigger timer") from e - # if not self._set_success: - # raise RuntimeError("Timer was not triggered") - # - # # -------------------------------------------------------------------- - # def trigger(self) -> AsyncStatus: - # watchers: List[Callable] = [] - # return AsyncStatus(self._trigger(watchers)) - - # -------------------------------------------------------------------- def prepare(self, p_time: float) -> AsyncStatus: return self.sampletime.set(p_time) From 856fc25274315faf42f61f7d500aaf6bff7a8158 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 5 Jul 2024 08:19:53 +0200 Subject: [PATCH 056/141] wip, fixing tests --- src/ophyd_async/tango/__init__.py | 4 +- src/ophyd_async/tango/signal/__init__.py | 4 +- src/ophyd_async/tango/signal/signal.py | 72 +++++++++++++----------- tests/tango/test_base_device.py | 20 +++---- tests/tango/test_tango_signals.py | 58 ++++++++++++------- 5 files changed, 87 insertions(+), 71 deletions(-) diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py index a9feff000d..88276fc73c 100644 --- a/src/ophyd_async/tango/__init__.py +++ b/src/ophyd_async/tango/__init__.py @@ -1,6 +1,6 @@ from ophyd_async.tango.base_devices import TangoReadableDevice from ophyd_async.tango.signal import ( -# tango_signal_auto, + tango_signal_auto, tango_signal_r, tango_signal_rw, tango_signal_w, @@ -12,6 +12,6 @@ "tango_signal_rw", "tango_signal_w", "tango_signal_x", -# "tango_signal_auto", + "tango_signal_auto", "TangoReadableDevice", ] diff --git a/src/ophyd_async/tango/signal/__init__.py b/src/ophyd_async/tango/signal/__init__.py index b55f7e5a1d..e006b3645c 100644 --- a/src/ophyd_async/tango/signal/__init__.py +++ b/src/ophyd_async/tango/signal/__init__.py @@ -1,5 +1,5 @@ from .signal import ( -# tango_signal_auto, + tango_signal_auto, tango_signal_r, tango_signal_rw, tango_signal_w, @@ -11,5 +11,5 @@ "tango_signal_rw", "tango_signal_w", "tango_signal_x", -# "tango_signal_auto", + "tango_signal_auto", ) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 2ca347abae..3629dca080 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -2,14 +2,22 @@ from __future__ import annotations -from typing import Optional, Type +from typing import Optional, Type, Union from ophyd_async.core import SignalR, SignalRW, SignalW, SignalX, T from ophyd_async.core.utils import DEFAULT_TIMEOUT from ophyd_async.tango._backend import TangoTransport +from tango import AttrWriteType, CmdArgType +from tango import DeviceProxy as SyncDeviceProxy from tango.asyncio import DeviceProxy -__all__ = ("tango_signal_rw", "tango_signal_r", "tango_signal_w", "tango_signal_x") +__all__ = ( + "tango_signal_rw", + "tango_signal_r", + "tango_signal_w", + "tango_signal_x", + "tango_signal_auto", +) def _make_backend( @@ -130,35 +138,31 @@ def tango_signal_x( # -------------------------------------------------------------------- -# def tango_signal_auto( -# datatype: Type[T], full_trl: str, device_proxy: Optional[DeviceProxy] = None -# ) -> Union[TangoSignalW, TangoSignalX, TangoSignalR, TangoSignalRW]: -# backend: TangoSignalBackend = TangoTransport( -# datatype, full_trl, full_trl, device_proxy -# ) - -# device_trl, tr_name = full_trl.rsplit("/", 1) -# device_proxy = SyncDeviceProxy(device_trl) -# if tr_name in device_proxy.get_attribute_list(): -# config = device_proxy.get_attribute_config(tr_name) -# if config.writable in [AttrWriteType.READ_WRITE, -# AttrWriteType.READ_WITH_WRITE]: -# return TangoSignalRW(backend) -# elif config.writable == AttrWriteType.READ: -# return TangoSignalR(backend) -# else: -# return TangoSignalW(backend) - -# if tr_name in device_proxy.get_command_list(): -# config = device_proxy.get_command_config(tr_name) -# if config.in_type == CmdArgType.DevVoid: -# return TangoSignalX(backend) -# elif config.out_type != CmdArgType.DevVoid: -# return TangoSignalRW(backend) -# else: -# return TangoSignalX(backend) - -# if tr_name in device_proxy.get_pipe_list(): -# raise NotImplementedError("Pipes are not supported") - -# raise RuntimeError(f"Cannot find {tr_name} in {device_trl}") +def tango_signal_auto( + datatype: Type[T], full_trl: str, device_proxy: Optional[DeviceProxy] = None +) -> Union[SignalW, SignalX, SignalR, SignalRW]: + device_trl, tr_name = full_trl.rsplit("/", 1) + syn_proxy = SyncDeviceProxy(device_trl) + backend = _make_backend(datatype, full_trl, full_trl, device_proxy) + if tr_name in syn_proxy.get_attribute_list(): + config = syn_proxy.get_attribute_config(tr_name) + if config.writable in [AttrWriteType.READ_WRITE, AttrWriteType.READ_WITH_WRITE]: + return SignalRW(backend) + elif config.writable == AttrWriteType.READ: + return SignalR(backend) + else: + return SignalW(backend) + + if tr_name in syn_proxy.get_command_list(): + config = syn_proxy.get_command_config(tr_name) + if config.in_type == CmdArgType.DevVoid: + return SignalX(backend) + elif config.out_type != CmdArgType.DevVoid: + return SignalRW(backend) + else: + return SignalX(backend) + + if tr_name in device_proxy.get_pipe_list(): + raise NotImplementedError("Pipes are not supported") + + raise RuntimeError(f"Cannot find {tr_name} in {device_trl}") diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index ab3b791248..399c9aea48 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -82,16 +82,12 @@ def write_limitedvalue(self, value: float): class TestReadableDevice(TangoReadableDevice): __test__ = False - # -------------------------------------------------------------------- - def register_signals(self): - for name in TESTED_FEATURES: - setattr( - self, name, tango_signal_auto(None, f"{self.trl}/{name}", self.proxy) - ) - - self.set_readable_signals( - read_uncached=[getattr(self, name) for name in TESTED_FEATURES] - ) + def __init__(self, trl: str, name="") -> None: + self.trl = trl + for feature in TESTED_FEATURES: + with self.add_children_as_readables(): + setattr(self, feature, tango_signal_auto(None, f"{trl}/{feature}")) + TangoReadableDevice.__init__(self, trl, name) # -------------------------------------------------------------------- @@ -200,7 +196,7 @@ async def test_connect(tango_test_device): values, description = describe_class(tango_test_device) async with DeviceCollector(): - test_device = await TestReadableDevice(tango_test_device) + test_device = TestReadableDevice(tango_test_device) assert test_device.name == "test_device" assert description == await test_device.describe() @@ -211,7 +207,7 @@ async def test_connect(tango_test_device): @pytest.mark.asyncio async def test_with_bluesky(tango_test_device): async with DeviceCollector(): - ophyd_dev = await TestReadableDevice(tango_test_device) + ophyd_dev = TestReadableDevice(tango_test_device) # now let's do some bluesky stuff RE = RunEngine() diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index 1878178541..a4800262f0 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -11,7 +11,7 @@ from bluesky.protocols import Reading from ophyd_async.core import SignalBackend, T -from ophyd_async.tango._backend import TangoSignalBackend, TangoTransport +from ophyd_async.tango._backend import TangoTransport from tango import AttrDataFormat, AttrWriteType, DeviceProxy, DevState from tango.asyncio_executor import set_global_executor from tango.server import Device, attribute, command @@ -218,9 +218,7 @@ def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: # -------------------------------------------------------------------- -async def make_backend( - typ: Optional[Type], pv: str, connect=True -) -> TangoSignalBackend: +async def make_backend(typ: Optional[Type], pv: str, connect=True) -> TangoTransport: backend = TangoTransport(typ, pv, pv) if connect: await asyncio.wait_for(backend.connect(), 10) @@ -235,22 +233,30 @@ def prepare_device(echo_device: str, pv: str, put_value: T) -> None: # -------------------------------------------------------------------- class MonitorQueue: def __init__(self, backend: SignalBackend): + print("Initializing MonitorQueue") self.updates: asyncio.Queue[Tuple[Reading, Any]] = asyncio.Queue() self.backend = backend self.subscription = backend.set_callback(self.add_reading_value) - # -------------------------------------------------------------------- def add_reading_value(self, reading: Reading, value): + print(f"Adding reading value: {value}") self.updates.put_nowait((reading, value)) - # -------------------------------------------------------------------- async def assert_updates(self, expected_value): + print(f"Asserting updates for expected value: {expected_value}") expected_reading = { "timestamp": pytest.approx(time.time(), rel=0.1), "alarm_severity": 0, } update_reading, update_value = await self.updates.get() get_reading = await self.backend.get_reading() + print(f"Update value: {update_value}, Expected value: {expected_value}") + print(f"Types: {type(update_value)}, {type(expected_value)}") + # If update_value is a numpy.ndarray, convert it to a list + if isinstance(update_value, np.ndarray): + update_value = update_value.tolist() + print(f"Update value: {update_value}, Expected value: {expected_value}") + print(f"Types: {type(update_value)}, {type(expected_value)}") assert_close(update_value, expected_value) assert_close(await self.backend.get_value(), expected_value) @@ -260,12 +266,13 @@ async def assert_updates(self, expected_value): get_reading = dict(get_reading) get_value = get_reading.pop("value") + print(f"Update reading: {update_reading}, Get reading: {get_reading}") assert update_reading == expected_reading == get_reading assert_close(update_value, expected_value) assert_close(get_value, expected_value) - # -------------------------------------------------------------------- def close(self): + print("Closing MonitorQueue and removing callback") self.backend.set_callback(None) @@ -284,12 +291,12 @@ async def assert_monitor_then_put( # Make a monitor queue that will monitor for updates q = MonitorQueue(backend) try: - assert dict(source=source, **descriptor) == await backend.get_descriptor() + assert dict(source=source, **descriptor) == await backend.get_datakey("") # Check initial value await q.assert_updates(initial_value) # Put to new value and check that - await backend.put(put_value) - assert_close(put_value, await backend.get_w_value()) + await backend.put(put_value, wait=True) + assert_close(put_value, await backend.get_setpoint()) await q.assert_updates(put_value) finally: q.close() @@ -302,6 +309,7 @@ async def assert_monitor_then_put( ATTRIBUTES_SET, ids=[x[0] for x in ATTRIBUTES_SET], ) +@pytest.mark.asyncio async def test_backend_get_put_monitor_attr( echo_device: str, pv: str, @@ -311,14 +319,23 @@ async def test_backend_get_put_monitor_attr( initial_value: T, put_value: T, ): - # With the given datatype, check we have the correct initial value and putting works - descriptor = get_test_descriptor(py_type, initial_value, False) - await assert_monitor_then_put( - echo_device, pv, initial_value, put_value, descriptor, py_type - ) - # # With guessed datatype, check we can set it back to the initial value - # await assert_monitor_then_put(echo_device, pv, initial_value, put_value, - # descriptor) + try: + # Set a timeout for the operation to prevent it from running indefinitely + await asyncio.wait_for( + assert_monitor_then_put( + echo_device, + pv, + initial_value, + put_value, + get_test_descriptor(py_type, initial_value, False), + py_type, + ), + timeout=10, # Timeout in seconds + ) + except asyncio.TimeoutError: + pytest.fail("Test timed out") + except Exception as e: + pytest.fail(f"Test failed with exception: {e}") # -------------------------------------------------------------------- @@ -332,9 +349,9 @@ async def assert_put_read( source = echo_device + "/" + pv backend = await make_backend(datatype, source) # Make a monitor queue that will monitor for updates - assert dict(source=source, **descriptor) == await backend.get_descriptor() + assert dict(source=source, **descriptor) == await backend.get_datakey("") # Put to new value and check that - await backend.put(put_value) + await backend.put(put_value, wait=True) expected_reading = { "timestamp": pytest.approx(time.time(), rel=0.1), @@ -364,7 +381,6 @@ async def test_backend_get_put_monitor_cmd( initial_value: T, put_value: T, ): - print("Starting test!") # With the given datatype, check we have the correct initial value and putting works descriptor = get_test_descriptor(py_type, initial_value, True) await assert_put_read(echo_device, pv, put_value, descriptor, py_type) From d8c8af3c3737d5159eb504abdd74c67949aa48fd Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 5 Jul 2024 13:30:11 +0200 Subject: [PATCH 057/141] Poll wasn't correctly handling if/else statements when checking value changes --- .../tango/_backend/_tango_transport.py | 81 ++++++++++++------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 33a3dc8f47..c4ab327b0e 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -268,40 +268,59 @@ async def poll(self): than the absolute or relative change. This function is used when an attribute that does not support events is cached or a callback is passed to it. """ - last_reading = await self.get_reading() - flag = 0 - # Initial reading - if self._callback is not None: - self._callback(last_reading, last_reading["value"]) - - while True: - await asyncio.sleep(self._polling_period) - reading = await self.get_reading() - if reading is None or reading["value"] is None: - continue - diff = abs(reading["value"] - last_reading["value"]) - - if self._abs_change is not None: - if diff > self._abs_change: - if self._callback is not None: - self._callback(reading, reading["value"]) - flag = 0 - - elif self._rel_change is not None: - if last_reading["value"] is not None: - if diff > self._rel_change * abs(last_reading["value"]): - if self._callback is not None: + try: + last_reading = await self.get_reading() + flag = 0 + # Initial reading + if self._callback is not None: + self._callback(last_reading, last_reading["value"]) + except Exception as e: + raise RuntimeError(f"Could not poll the attribute") from e + + try: + # If the value is a number, we can check for changes + if isinstance(last_reading["value"], (int, float)): + while True: + await asyncio.sleep(self._polling_period) + reading = await self.get_reading() + if reading is None or reading["value"] is None: + reading = last_reading.copy() + continue + diff = abs(reading["value"] - last_reading["value"]) + if self._abs_change is not None and diff >= abs(self._abs_change): + if self._callback is not None: + self._callback(reading, reading["value"]) + flag = 0 + + elif self._rel_change is not None and diff >= self._rel_change * abs(last_reading["value"]): + if self._callback is not None: + self._callback(reading, reading["value"]) + flag = 0 + + else: + flag = (flag + 1) % 4 + if flag == 0 and self._callback is not None: self._callback(reading, reading["value"]) - flag = 0 + last_reading = reading.copy() + if self._callback is None: + break + # If the value is not a number, we can only poll else: - flag = (flag + 1) % 4 - if flag == 0 and self._callback is not None: - self._callback(reading, reading["value"]) - - last_reading = reading.copy() - if self._callback is None: - break + while True: + await asyncio.sleep(self._polling_period) + flag = (flag + 1) % 4 + if flag == 0: + reading = await self.get_reading() + if reading is None or reading["value"] is None: + continue + last_reading = reading.copy() + if self._callback is not None: + self._callback(reading, reading["value"]) + else: + break + except Exception as e: + raise RuntimeError(f"Could not poll the attribute") from e # -------------------------------------------------------------------- def set_polling( From 06a9911f6aa3806a6ca14d11e5e74dd20d7f0f29 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 5 Jul 2024 13:30:51 +0200 Subject: [PATCH 058/141] Removed unused TangoMover --- src/ophyd_async/tango/device_controllers/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ophyd_async/tango/device_controllers/__init__.py b/src/ophyd_async/tango/device_controllers/__init__.py index 5812e13ebf..59cf2d18f6 100644 --- a/src/ophyd_async/tango/device_controllers/__init__.py +++ b/src/ophyd_async/tango/device_controllers/__init__.py @@ -1,6 +1,5 @@ from ophyd_async.tango.device_controllers.dgg2 import DGG2Timer from ophyd_async.tango.device_controllers.omsvme58 import OmsVME58Motor from ophyd_async.tango.device_controllers.sis3820 import SIS3820Counter -from ophyd_async.tango.device_controllers.tango_mover import TangoMover -__all__ = ("OmsVME58Motor", "SIS3820Counter", "DGG2Timer", "TangoMover") +__all__ = ("OmsVME58Motor", "SIS3820Counter", "DGG2Timer") From 4a8aa4fb2135d53d1d349f7f326080f976d5cd15 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 5 Jul 2024 13:31:37 +0200 Subject: [PATCH 059/141] Removed tests that explicitly use external running tango servers --- tests/tango/test_ddg2.py | 62 ------------------ tests/tango/test_motors.py | 127 ------------------------------------ tests/tango/test_scans.py | 66 ------------------- tests/tango/test_sis3820.py | 54 --------------- 4 files changed, 309 deletions(-) delete mode 100644 tests/tango/test_ddg2.py delete mode 100644 tests/tango/test_motors.py delete mode 100644 tests/tango/test_scans.py delete mode 100644 tests/tango/test_sis3820.py diff --git a/tests/tango/test_ddg2.py b/tests/tango/test_ddg2.py deleted file mode 100644 index b0f0c7aae7..0000000000 --- a/tests/tango/test_ddg2.py +++ /dev/null @@ -1,62 +0,0 @@ -from unittest.mock import Mock - -import pytest -from bluesky import Msg, RunEngine -from bluesky.plans import count - -from ophyd_async.core import DeviceCollector -from ophyd_async.tango.device_controllers import DGG2Timer -from tango.asyncio_executor import set_global_executor - - -# -------------------------------------------------------------------- -@pytest.fixture(autouse=True) -def reset_tango_asyncio(): - set_global_executor(None) - - -# -------------------------------------------------------------------- -@pytest.fixture -async def dgg2(): - async with DeviceCollector(): - dgg2 = await DGG2Timer("p09/dgg2/eh.01") - - yield dgg2 - - -# -------------------------------------------------------------------- -@pytest.mark.asyncio -async def test_connect(dgg2): - assert dgg2.name == "dgg2" - - -# -------------------------------------------------------------------- -@pytest.mark.asyncio -async def test_readout_with_bluesky(dgg2): - TEST_TIME = 0.1 - - await dgg2.set_time(TEST_TIME) - readouts = Mock() - - RE = RunEngine() - - RE([Msg("prepare", dgg2, TEST_TIME)]) - RE(count([dgg2], 3), readouts) - - description = [ - args[0][1]["configuration"] - for args in readouts.call_args_list - if args[0][0] == "descriptor" - ][0] - assert "dgg2" in description - assert "dgg2-sampletime" in description["dgg2"]["data"] - assert description["dgg2"]["data"]["dgg2-sampletime"] == pytest.approx( - TEST_TIME, abs=0.1 - ) - - readings = [ - args[0][1]["data"] for args in readouts.call_args_list if args[0][0] == "event" - ] - assert len(readings) == 3 - assert "dgg2-sampletime" in readings[0] - assert readings[0]["dgg2-sampletime"] == pytest.approx(TEST_TIME, abs=0.1) diff --git a/tests/tango/test_motors.py b/tests/tango/test_motors.py deleted file mode 100644 index 85355f86d0..0000000000 --- a/tests/tango/test_motors.py +++ /dev/null @@ -1,127 +0,0 @@ -import asyncio -from unittest.mock import Mock, call - -import numpy as np -import pytest -from bluesky import RunEngine -from bluesky.plan_stubs import mv -from bluesky.plans import count, scan - -from ophyd_async.core import DeviceCollector -from ophyd_async.tango.device_controllers import OmsVME58Motor -from ophyd_async.tango.sardana import SardanaMotor -from tango.asyncio_executor import set_global_executor - -# Long enough for multiple asyncio event loop cycles to run so -# all the tasks have a chance to run -A_BIT = 0.001 - -# dict: {class: tango trl} -MOTORS_TO_TEST = { - SardanaMotor: "motor/dummy_mot_ctrl/1", - OmsVME58Motor: "p09/motor/eh.01", -} - - -# -------------------------------------------------------------------- -@pytest.fixture( - params=list(MOTORS_TO_TEST.items()), - ids=list(MOTORS_TO_TEST.keys()), -) -def motor_to_test(request): - return request.param - - -# -------------------------------------------------------------------- -@pytest.fixture(autouse=True) -def reset_tango_asyncio(): - set_global_executor(None) - - -# -------------------------------------------------------------------- -@pytest.fixture -async def dummy_motor(motor_to_test): - async with DeviceCollector(): - dummy_motor = await motor_to_test[0](motor_to_test[1]) - - yield dummy_motor - - -# -------------------------------------------------------------------- -@pytest.mark.asyncio -async def test_connect(dummy_motor): - assert dummy_motor.name == "dummy_motor" - - -# -------------------------------------------------------------------- -@pytest.mark.asyncio -async def test_readout_with_bluesky(dummy_motor): - # now let's do some bluesky stuff - RE = RunEngine() - RE(count([dummy_motor], 1)) - - -# -------------------------------------------------------------------- -@pytest.mark.asyncio -async def test_move(dummy_motor): - start_position = await dummy_motor.position.get_value() - target_position = start_position + 1 - - status = dummy_motor.set(target_position) - watcher = Mock() - status.watch(watcher) - done = Mock() - status.add_callback(done) - await asyncio.sleep(A_BIT) - assert watcher.call_count == 1 - assert watcher.call_args == call( - name="dummy_motor", - current=start_position, - initial=start_position, - target=target_position, - time_elapsed=pytest.approx(0.0, abs=0.05), - ) - await status - assert ( - pytest.approx(target_position, abs=0.1) - == await dummy_motor.position.get_value() - ) - assert status.done - done.assert_called_once_with(status) - - -# -------------------------------------------------------------------- -@pytest.mark.asyncio -async def test_move_with_bluesky(dummy_motor): - start_position = await dummy_motor.position.get_value() - target_position = start_position + 1 - - # now let's do some bluesky stuff - RE = RunEngine() - RE(mv(dummy_motor, target_position)) - - -# -------------------------------------------------------------------- -@pytest.mark.asyncio -async def test_scan_motor_vs_motor_position(dummy_motor): - readouts = Mock() - - # now let's do some bluesky stuff - RE = RunEngine() - RE(scan([dummy_motor.position], dummy_motor, 0, 1, num=11), readouts) - - assert readouts.call_count == 14 - assert {args[0][0] for args in readouts.call_args_list} == { - "descriptor", - "event", - "start", - "stop", - } - - positions = [ - args[0][1]["data"]["dummy_motor-position"] - for args in readouts.call_args_list - if args[0][0] == "event" - ] - for got, expected in zip(positions, np.arange(0, 1.1, 0.1)): - assert pytest.approx(got, abs=0.1) == expected diff --git a/tests/tango/test_scans.py b/tests/tango/test_scans.py deleted file mode 100644 index 211d6cb1df..0000000000 --- a/tests/tango/test_scans.py +++ /dev/null @@ -1,66 +0,0 @@ -from unittest.mock import Mock - -import numpy as np -import pytest -from bluesky import RunEngine -from bluesky.plans import scan - -from ophyd_async.core import DeviceCollector -from ophyd_async.tango.device_controllers import ( - DGG2Timer, - OmsVME58Motor, - SIS3820Counter, -) -from tango.asyncio_executor import set_global_executor - -# Long enough for multiple asyncio event loop cycles to run so -# all the tasks have a chance to run -A_BIT = 0.001 - - -# -------------------------------------------------------------------- -@pytest.fixture(autouse=True) -def reset_tango_asyncio(): - set_global_executor(None) - - -# -------------------------------------------------------------------- -@pytest.fixture -async def devices_set(): - async with DeviceCollector(): - omsvme58_motor = await OmsVME58Motor("p09/motor/eh.01") - dgg2timer = await DGG2Timer("p09/dgg2/eh.01") - sis3820 = await SIS3820Counter("p09/counter/eh.01") - - yield [omsvme58_motor, dgg2timer, sis3820] - - -# -------------------------------------------------------------------- -@pytest.mark.asyncio -async def test_step_scan_motor_vs_counter_with_dgg2(devices_set): - omsvme58_motor, dgg2timer, sis3820 = devices_set - - readouts = Mock() - - # now let's do some bluesky stuff - RE = RunEngine() - RE( - scan([omsvme58_motor, sis3820, dgg2timer], omsvme58_motor, 0, 1, num=11), - readouts, - ) - - assert readouts.call_count == 14 - assert {arg[0][0] for arg in readouts.call_args_list} == { - "descriptor", - "event", - "start", - "stop", - } - - positions = [ - args[0][1]["data"]["omsvme58_motor-position"] - for args in readouts.call_args_list - if args[0][0] == "event" - ] - for got, expected in zip(positions, np.arange(0, 1.1, 0.1)): - assert pytest.approx(got, abs=0.1) == expected diff --git a/tests/tango/test_sis3820.py b/tests/tango/test_sis3820.py deleted file mode 100644 index e7ca2d2169..0000000000 --- a/tests/tango/test_sis3820.py +++ /dev/null @@ -1,54 +0,0 @@ -from unittest.mock import Mock - -import pytest -from bluesky import RunEngine -from bluesky.plans import count - -from ophyd_async.core import DeviceCollector -from ophyd_async.tango.device_controllers import SIS3820Counter -from tango.asyncio_executor import set_global_executor - - -# -------------------------------------------------------------------- -@pytest.fixture(autouse=True) -def reset_tango_asyncio(): - set_global_executor(None) - - -# -------------------------------------------------------------------- -@pytest.fixture -async def sis3820(): - async with DeviceCollector(): - sis3820 = await SIS3820Counter("p09/counter/eh.01") - - yield sis3820 - - -# -------------------------------------------------------------------- -@pytest.mark.asyncio -async def test_connect(sis3820): - assert sis3820.name == "sis3820" - - -# -------------------------------------------------------------------- -@pytest.mark.asyncio -async def test_readout_with_bluesky(sis3820): - readouts = Mock() - # now let's do some bluesky stuff - - RE = RunEngine() - RE(count([sis3820], 3), readouts) - - description = [ - args[0][1]["configuration"] - for args in readouts.call_args_list - if args[0][0] == "descriptor" - ][0] - assert "sis3820" in description - assert "sis3820-offset" in description["sis3820"]["data"] - - readings = [ - args[0][1]["data"] for args in readouts.call_args_list if args[0][0] == "event" - ] - assert len(readings) == 3 - assert "sis3820-counts" in readings[0] From ec03fdf6d002e1e181bad838bfeee19a0cd404ea Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 5 Jul 2024 13:35:11 +0200 Subject: [PATCH 060/141] linting fix --- .../tango/_backend/_tango_transport.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index c4ab327b0e..d8c2c66938 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -275,7 +275,7 @@ async def poll(self): if self._callback is not None: self._callback(last_reading, last_reading["value"]) except Exception as e: - raise RuntimeError(f"Could not poll the attribute") from e + raise RuntimeError("Could not poll the attribute") from e try: # If the value is a number, we can check for changes @@ -288,14 +288,17 @@ async def poll(self): continue diff = abs(reading["value"] - last_reading["value"]) if self._abs_change is not None and diff >= abs(self._abs_change): - if self._callback is not None: - self._callback(reading, reading["value"]) - flag = 0 + if self._callback is not None: + self._callback(reading, reading["value"]) + flag = 0 - elif self._rel_change is not None and diff >= self._rel_change * abs(last_reading["value"]): - if self._callback is not None: - self._callback(reading, reading["value"]) - flag = 0 + elif ( + self._rel_change is not None + and diff >= self._rel_change * abs(last_reading["value"]) + ): + if self._callback is not None: + self._callback(reading, reading["value"]) + flag = 0 else: flag = (flag + 1) % 4 @@ -320,7 +323,7 @@ async def poll(self): else: break except Exception as e: - raise RuntimeError(f"Could not poll the attribute") from e + raise RuntimeError("Could not poll the attribute") from e # -------------------------------------------------------------------- def set_polling( From 7648fc44da9bc6edae8224834945c0cb67764416 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 5 Jul 2024 13:38:36 +0200 Subject: [PATCH 061/141] Modified testing for new backend. --- tests/tango/test_tango_signals.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index a4800262f0..a2462edfbf 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -45,7 +45,6 @@ class TestEnum(IntEnum): ("string", "DevString", str, ("aaa", "bbb", "ccc")), ("state", "DevState", DevState, (DevState.ON, DevState.MOVING, DevState.ALARM)), ("enum", "DevEnum", TestEnum, (TestEnum.A, TestEnum.B)), - # ("encoded", 'DevEncoded', TestEnum, (TestEnum.A, TestEnum.B)), ) ATTRIBUTES_SET = [] @@ -218,8 +217,14 @@ def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: # -------------------------------------------------------------------- -async def make_backend(typ: Optional[Type], pv: str, connect=True) -> TangoTransport: +async def make_backend( + typ: Optional[Type], + pv: str, + connect: bool = True, + allow_events: Optional[bool] = True, +) -> TangoTransport: backend = TangoTransport(typ, pv, pv) + backend.allow_events(allow_events) if connect: await asyncio.wait_for(backend.connect(), 10) return backend @@ -233,30 +238,23 @@ def prepare_device(echo_device: str, pv: str, put_value: T) -> None: # -------------------------------------------------------------------- class MonitorQueue: def __init__(self, backend: SignalBackend): - print("Initializing MonitorQueue") self.updates: asyncio.Queue[Tuple[Reading, Any]] = asyncio.Queue() self.backend = backend self.subscription = backend.set_callback(self.add_reading_value) def add_reading_value(self, reading: Reading, value): - print(f"Adding reading value: {value}") self.updates.put_nowait((reading, value)) async def assert_updates(self, expected_value): - print(f"Asserting updates for expected value: {expected_value}") expected_reading = { "timestamp": pytest.approx(time.time(), rel=0.1), "alarm_severity": 0, } update_reading, update_value = await self.updates.get() get_reading = await self.backend.get_reading() - print(f"Update value: {update_value}, Expected value: {expected_value}") - print(f"Types: {type(update_value)}, {type(expected_value)}") # If update_value is a numpy.ndarray, convert it to a list if isinstance(update_value, np.ndarray): update_value = update_value.tolist() - print(f"Update value: {update_value}, Expected value: {expected_value}") - print(f"Types: {type(update_value)}, {type(expected_value)}") assert_close(update_value, expected_value) assert_close(await self.backend.get_value(), expected_value) @@ -266,13 +264,11 @@ async def assert_updates(self, expected_value): get_reading = dict(get_reading) get_value = get_reading.pop("value") - print(f"Update reading: {update_reading}, Get reading: {get_reading}") assert update_reading == expected_reading == get_reading assert_close(update_value, expected_value) assert_close(get_value, expected_value) def close(self): - print("Closing MonitorQueue and removing callback") self.backend.set_callback(None) @@ -287,7 +283,7 @@ async def assert_monitor_then_put( ): prepare_device(echo_device, pv, initial_value) source = echo_device + "/" + pv - backend = await make_backend(datatype, source) + backend = await make_backend(datatype, source, allow_events=True) # Make a monitor queue that will monitor for updates q = MonitorQueue(backend) try: @@ -386,3 +382,6 @@ async def test_backend_get_put_monitor_cmd( await assert_put_read(echo_device, pv, put_value, descriptor, py_type) # # With guessed datatype, check we can set it back to the initial value await assert_put_read(echo_device, pv, put_value, descriptor) + tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] + await asyncio.gather(*tasks) + del echo_device From 1333d0e1328d6e14d79eab67c57a2c5e76d9e975 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 8 Jul 2024 11:41:31 +0200 Subject: [PATCH 062/141] Modified tests to use asynchronous DeviceProxy --- tests/tango/test_base_device.py | 24 +++++++++++++++--------- tests/tango/test_tango_signals.py | 10 ++++++---- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 399c9aea48..796e392ef1 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -15,9 +15,10 @@ AttrQuality, AttrWriteType, CmdArgType, - DeviceProxy, + # DeviceProxy, DevState, ) +from tango.asyncio import DeviceProxy from tango.asyncio_executor import set_global_executor from tango.server import Device, attribute from tango.test_context import MultiDeviceTestContext @@ -91,15 +92,16 @@ def __init__(self, trl: str, name="") -> None: # -------------------------------------------------------------------- -def describe_class(fqtrl): +async def describe_class(fqtrl): description = {} values = {} - dev = DeviceProxy(fqtrl) + dev = await DeviceProxy(fqtrl) for name in TESTED_FEATURES: if name in dev.get_attribute_list(): - attr_conf = dev.get_attribute_config(name) - value = dev.read_attribute(name).value + attr_conf = await dev.get_attribute_config(name) + attr_value = await dev.read_attribute(name) + value = attr_value.value _, _, descr = get_python_type(attr_conf.data_type) max_x = attr_conf.max_dim_x max_y = attr_conf.max_dim_y @@ -114,7 +116,7 @@ def describe_class(fqtrl): shape = [max_y, max_x] elif name in dev.get_command_list(): - cmd_conf = dev.get_command_config(name) + cmd_conf = await dev.get_command_config(name) _, _, descr = get_python_type( cmd_conf.in_type if cmd_conf.in_type != CmdArgType.DevVoid @@ -193,7 +195,7 @@ def compare_values(expected, received): # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_connect(tango_test_device): - values, description = describe_class(tango_test_device) + values, description = await describe_class(tango_test_device) async with DeviceCollector(): test_device = TestReadableDevice(tango_test_device) @@ -206,8 +208,12 @@ async def test_connect(tango_test_device): # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_with_bluesky(tango_test_device): - async with DeviceCollector(): - ophyd_dev = TestReadableDevice(tango_test_device) + async def connect(): + async with DeviceCollector(): + device = TestReadableDevice(tango_test_device) + return device + + ophyd_dev = await connect() # now let's do some bluesky stuff RE = RunEngine() diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index a2462edfbf..e292ca231a 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -12,7 +12,8 @@ from ophyd_async.core import SignalBackend, T from ophyd_async.tango._backend import TangoTransport -from tango import AttrDataFormat, AttrWriteType, DeviceProxy, DevState +from tango import AttrDataFormat, AttrWriteType, DevState +from tango.asyncio import DeviceProxy from tango.asyncio_executor import set_global_executor from tango.server import Device, attribute, command from tango.test_context import MultiDeviceTestContext @@ -231,8 +232,9 @@ async def make_backend( # -------------------------------------------------------------------- -def prepare_device(echo_device: str, pv: str, put_value: T) -> None: - setattr(DeviceProxy(echo_device), pv, put_value) +async def prepare_device(echo_device: str, pv: str, put_value: T) -> None: + proxy = await DeviceProxy(echo_device) + setattr(proxy, pv, put_value) # -------------------------------------------------------------------- @@ -281,7 +283,7 @@ async def assert_monitor_then_put( descriptor: dict, datatype: Optional[Type[T]] = None, ): - prepare_device(echo_device, pv, initial_value) + await prepare_device(echo_device, pv, initial_value) source = echo_device + "/" + pv backend = await make_backend(datatype, source, allow_events=True) # Make a monitor queue that will monitor for updates From dc2cf080acbbb81b2373a78314b6219c1915aade Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 8 Jul 2024 13:09:12 +0200 Subject: [PATCH 063/141] Added pytango to dependency list --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4090bfb30d..cf59c4750f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ dev = [ "pydata-sphinx-theme>=0.12", "pyepics>=3.4.2", "pyside6==6.7.0", + "pytango", "pytest", "pytest-asyncio", "pytest-cov", From f08a213c12f741df5f6ff62bab010a366649f57d Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 8 Jul 2024 14:09:42 +0200 Subject: [PATCH 064/141] Removed redundant decorator --- tests/tango/test_tango_signals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index e292ca231a..467ea1236f 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -307,7 +307,6 @@ async def assert_monitor_then_put( ATTRIBUTES_SET, ids=[x[0] for x in ATTRIBUTES_SET], ) -@pytest.mark.asyncio async def test_backend_get_put_monitor_attr( echo_device: str, pv: str, From 7c112fc038718886cb2a799375a71ca76eed0708 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 10 Jul 2024 11:25:25 +0200 Subject: [PATCH 065/141] Added back in optional call to register_signals() after the proxy is awaited in TangoReadableDevice.connect(). This allows for dynamically registered signals based on the state of the Tango Server. --- src/ophyd_async/tango/base_devices/base_device.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index 026aaeb3aa..590191a005 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -46,6 +46,11 @@ async def closure(): return self await closure() + + # If a register_signals method is defined, call it + if hasattr(self, "register_signals"): + self.register_signals() + await super().connect(mock=mock, timeout=timeout) # -------------------------------------------------------------------- From 219d2d9c5692ae0e84a957af57dc891747cb89ba Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 10 Jul 2024 14:47:08 +0200 Subject: [PATCH 066/141] Fixed docs by removing old generated files. --- .../ophyd_async.core.AsyncStatus.rst | 40 -------- .../ophyd_async.core.DetectorControl.rst | 32 ------- .../ophyd_async.core.DetectorGroupLogic.rst | 36 ------- .../ophyd_async.core.DetectorTrigger.rst | 85 ----------------- .../ophyd_async.core.DetectorWriter.rst | 34 ------- .../generated/ophyd_async.core.Device.rst | 39 -------- .../ophyd_async.core.DeviceCollector.rst | 29 ------ .../ophyd_async.core.DeviceVector.rst | 50 ---------- .../ophyd_async.core.DirectoryInfo.rst | 38 -------- .../ophyd_async.core.DirectoryProvider.rst | 29 ------ ...yd_async.core.HardwareTriggeredFlyable.rst | 48 ---------- .../ophyd_async.core.NameProvider.rst | 29 ------ .../ophyd_async.core.NotConnected.rst | 6 -- ...ync.core.SameTriggerDetectorGroupLogic.rst | 36 ------- .../ophyd_async.core.ShapeProvider.rst | 29 ------ .../generated/ophyd_async.core.Signal.rst | 40 -------- .../ophyd_async.core.SignalBackend.rst | 43 --------- .../generated/ophyd_async.core.SignalR.rst | 48 ---------- .../generated/ophyd_async.core.SignalRW.rst | 50 ---------- .../generated/ophyd_async.core.SignalW.rst | 41 -------- .../generated/ophyd_async.core.SignalX.rst | 41 -------- .../ophyd_async.core.SimSignalBackend.rst | 43 --------- .../ophyd_async.core.StandardDetector.rst | 55 ----------- .../ophyd_async.core.StandardReadable.rst | 46 --------- ...hyd_async.core.StaticDirectoryProvider.rst | 29 ------ .../ophyd_async.core.TriggerInfo.rst | 38 -------- .../ophyd_async.core.TriggerLogic.rst | 33 ------- .../generated/ophyd_async.core.get_dtype.rst | 6 -- .../ophyd_async.core.get_signal_values.rst | 6 -- .../generated/ophyd_async.core.get_unique.rst | 6 -- .../ophyd_async.core.load_device.rst | 6 -- .../ophyd_async.core.load_from_yaml.rst | 6 -- .../ophyd_async.core.merge_gathered_dicts.rst | 6 -- .../ophyd_async.core.observe_value.rst | 6 -- docs/user/generated/ophyd_async.core.rst | 92 ------------------ .../ophyd_async.core.save_device.rst | 6 -- .../ophyd_async.core.save_to_yaml.rst | 6 -- ...phyd_async.core.set_and_wait_for_value.rst | 6 -- .../ophyd_async.core.set_signal_values.rst | 6 -- .../ophyd_async.core.set_sim_callback.rst | 6 -- .../ophyd_async.core.set_sim_put_proceeds.rst | 6 -- .../ophyd_async.core.set_sim_value.rst | 6 -- .../ophyd_async.core.wait_for_connection.rst | 6 -- .../ophyd_async.core.wait_for_value.rst | 6 -- .../ophyd_async.core.walk_rw_signals.rst | 6 -- ...async.epics.areadetector.FileWriteMode.rst | 84 ----------------- ...hyd_async.epics.areadetector.ImageMode.rst | 84 ----------------- ...epics.areadetector.NDAttributeDataType.rst | 84 ----------------- ...ync.epics.areadetector.NDAttributesXML.rst | 31 ------ ...nc.epics.areadetector.SingleTriggerDet.rst | 47 ---------- .../ophyd_async.epics.areadetector.ad_r.rst | 6 -- .../ophyd_async.epics.areadetector.ad_rw.rst | 6 -- .../ophyd_async.epics.areadetector.rst | 50 ---------- .../ophyd_async.epics.demo.EnergyMode.rst | 83 ---------------- .../ophyd_async.epics.demo.Mover.rst | 49 ---------- .../ophyd_async.epics.demo.SampleStage.rst | 39 -------- .../ophyd_async.epics.demo.Sensor.rst | 46 --------- ...demo_ad_sim_detector.DemoADSimDetector.rst | 55 ----------- ..._async.epics.demo.demo_ad_sim_detector.rst | 37 -------- .../user/generated/ophyd_async.epics.demo.rst | 55 ----------- ..._async.epics.demo.start_ioc_subprocess.rst | 6 -- .../ophyd_async.epics.motion.Motor.rst | 49 ---------- .../generated/ophyd_async.epics.motion.rst | 37 -------- .../ophyd_async.epics.pvi.PVIEntry.rst | 50 ---------- .../ophyd_async.epics.pvi.make_signal.rst | 6 -- .../ophyd_async.epics.pvi.pvi_get.rst | 6 -- docs/user/generated/ophyd_async.epics.pvi.rst | 46 --------- docs/user/generated/ophyd_async.epics.rst | 39 -------- ...phyd_async.epics.signal.epics_signal_r.rst | 6 -- ...hyd_async.epics.signal.epics_signal_rw.rst | 6 -- ...phyd_async.epics.signal.epics_signal_w.rst | 6 -- ...phyd_async.epics.signal.epics_signal_x.rst | 6 -- .../ophyd_async.epics.signal.pvi_get.rst | 6 -- .../generated/ophyd_async.epics.signal.rst | 39 -------- .../generated/ophyd_async.panda.PVIEntry.rst | 50 ---------- .../generated/ophyd_async.panda.PandA.rst | 44 --------- .../ophyd_async.panda.PandaPcapController.rst | 32 ------- .../generated/ophyd_async.panda.PcapBlock.rst | 41 -------- .../ophyd_async.panda.PulseBlock.rst | 41 -------- .../generated/ophyd_async.panda.SeqBlock.rst | 41 -------- .../generated/ophyd_async.panda.SeqTable.rst | 62 ------------ .../ophyd_async.panda.SeqTableRow.rst | 51 ---------- .../ophyd_async.panda.SeqTrigger.rst | 94 ------------------- .../ophyd_async.panda.phase_sorter.rst | 6 -- docs/user/generated/ophyd_async.panda.pvi.rst | 6 -- docs/user/generated/ophyd_async.panda.rst | 55 ----------- ...phyd_async.panda.seq_table_from_arrays.rst | 6 -- .../ophyd_async.panda.seq_table_from_rows.rst | 6 -- 88 files changed, 2860 deletions(-) delete mode 100644 docs/user/generated/ophyd_async.core.AsyncStatus.rst delete mode 100644 docs/user/generated/ophyd_async.core.DetectorControl.rst delete mode 100644 docs/user/generated/ophyd_async.core.DetectorGroupLogic.rst delete mode 100644 docs/user/generated/ophyd_async.core.DetectorTrigger.rst delete mode 100644 docs/user/generated/ophyd_async.core.DetectorWriter.rst delete mode 100644 docs/user/generated/ophyd_async.core.Device.rst delete mode 100644 docs/user/generated/ophyd_async.core.DeviceCollector.rst delete mode 100644 docs/user/generated/ophyd_async.core.DeviceVector.rst delete mode 100644 docs/user/generated/ophyd_async.core.DirectoryInfo.rst delete mode 100644 docs/user/generated/ophyd_async.core.DirectoryProvider.rst delete mode 100644 docs/user/generated/ophyd_async.core.HardwareTriggeredFlyable.rst delete mode 100644 docs/user/generated/ophyd_async.core.NameProvider.rst delete mode 100644 docs/user/generated/ophyd_async.core.NotConnected.rst delete mode 100644 docs/user/generated/ophyd_async.core.SameTriggerDetectorGroupLogic.rst delete mode 100644 docs/user/generated/ophyd_async.core.ShapeProvider.rst delete mode 100644 docs/user/generated/ophyd_async.core.Signal.rst delete mode 100644 docs/user/generated/ophyd_async.core.SignalBackend.rst delete mode 100644 docs/user/generated/ophyd_async.core.SignalR.rst delete mode 100644 docs/user/generated/ophyd_async.core.SignalRW.rst delete mode 100644 docs/user/generated/ophyd_async.core.SignalW.rst delete mode 100644 docs/user/generated/ophyd_async.core.SignalX.rst delete mode 100644 docs/user/generated/ophyd_async.core.SimSignalBackend.rst delete mode 100644 docs/user/generated/ophyd_async.core.StandardDetector.rst delete mode 100644 docs/user/generated/ophyd_async.core.StandardReadable.rst delete mode 100644 docs/user/generated/ophyd_async.core.StaticDirectoryProvider.rst delete mode 100644 docs/user/generated/ophyd_async.core.TriggerInfo.rst delete mode 100644 docs/user/generated/ophyd_async.core.TriggerLogic.rst delete mode 100644 docs/user/generated/ophyd_async.core.get_dtype.rst delete mode 100644 docs/user/generated/ophyd_async.core.get_signal_values.rst delete mode 100644 docs/user/generated/ophyd_async.core.get_unique.rst delete mode 100644 docs/user/generated/ophyd_async.core.load_device.rst delete mode 100644 docs/user/generated/ophyd_async.core.load_from_yaml.rst delete mode 100644 docs/user/generated/ophyd_async.core.merge_gathered_dicts.rst delete mode 100644 docs/user/generated/ophyd_async.core.observe_value.rst delete mode 100644 docs/user/generated/ophyd_async.core.rst delete mode 100644 docs/user/generated/ophyd_async.core.save_device.rst delete mode 100644 docs/user/generated/ophyd_async.core.save_to_yaml.rst delete mode 100644 docs/user/generated/ophyd_async.core.set_and_wait_for_value.rst delete mode 100644 docs/user/generated/ophyd_async.core.set_signal_values.rst delete mode 100644 docs/user/generated/ophyd_async.core.set_sim_callback.rst delete mode 100644 docs/user/generated/ophyd_async.core.set_sim_put_proceeds.rst delete mode 100644 docs/user/generated/ophyd_async.core.set_sim_value.rst delete mode 100644 docs/user/generated/ophyd_async.core.wait_for_connection.rst delete mode 100644 docs/user/generated/ophyd_async.core.wait_for_value.rst delete mode 100644 docs/user/generated/ophyd_async.core.walk_rw_signals.rst delete mode 100644 docs/user/generated/ophyd_async.epics.areadetector.FileWriteMode.rst delete mode 100644 docs/user/generated/ophyd_async.epics.areadetector.ImageMode.rst delete mode 100644 docs/user/generated/ophyd_async.epics.areadetector.NDAttributeDataType.rst delete mode 100644 docs/user/generated/ophyd_async.epics.areadetector.NDAttributesXML.rst delete mode 100644 docs/user/generated/ophyd_async.epics.areadetector.SingleTriggerDet.rst delete mode 100644 docs/user/generated/ophyd_async.epics.areadetector.ad_r.rst delete mode 100644 docs/user/generated/ophyd_async.epics.areadetector.ad_rw.rst delete mode 100644 docs/user/generated/ophyd_async.epics.areadetector.rst delete mode 100644 docs/user/generated/ophyd_async.epics.demo.EnergyMode.rst delete mode 100644 docs/user/generated/ophyd_async.epics.demo.Mover.rst delete mode 100644 docs/user/generated/ophyd_async.epics.demo.SampleStage.rst delete mode 100644 docs/user/generated/ophyd_async.epics.demo.Sensor.rst delete mode 100644 docs/user/generated/ophyd_async.epics.demo.demo_ad_sim_detector.DemoADSimDetector.rst delete mode 100644 docs/user/generated/ophyd_async.epics.demo.demo_ad_sim_detector.rst delete mode 100644 docs/user/generated/ophyd_async.epics.demo.rst delete mode 100644 docs/user/generated/ophyd_async.epics.demo.start_ioc_subprocess.rst delete mode 100644 docs/user/generated/ophyd_async.epics.motion.Motor.rst delete mode 100644 docs/user/generated/ophyd_async.epics.motion.rst delete mode 100644 docs/user/generated/ophyd_async.epics.pvi.PVIEntry.rst delete mode 100644 docs/user/generated/ophyd_async.epics.pvi.make_signal.rst delete mode 100644 docs/user/generated/ophyd_async.epics.pvi.pvi_get.rst delete mode 100644 docs/user/generated/ophyd_async.epics.pvi.rst delete mode 100644 docs/user/generated/ophyd_async.epics.rst delete mode 100644 docs/user/generated/ophyd_async.epics.signal.epics_signal_r.rst delete mode 100644 docs/user/generated/ophyd_async.epics.signal.epics_signal_rw.rst delete mode 100644 docs/user/generated/ophyd_async.epics.signal.epics_signal_w.rst delete mode 100644 docs/user/generated/ophyd_async.epics.signal.epics_signal_x.rst delete mode 100644 docs/user/generated/ophyd_async.epics.signal.pvi_get.rst delete mode 100644 docs/user/generated/ophyd_async.epics.signal.rst delete mode 100644 docs/user/generated/ophyd_async.panda.PVIEntry.rst delete mode 100644 docs/user/generated/ophyd_async.panda.PandA.rst delete mode 100644 docs/user/generated/ophyd_async.panda.PandaPcapController.rst delete mode 100644 docs/user/generated/ophyd_async.panda.PcapBlock.rst delete mode 100644 docs/user/generated/ophyd_async.panda.PulseBlock.rst delete mode 100644 docs/user/generated/ophyd_async.panda.SeqBlock.rst delete mode 100644 docs/user/generated/ophyd_async.panda.SeqTable.rst delete mode 100644 docs/user/generated/ophyd_async.panda.SeqTableRow.rst delete mode 100644 docs/user/generated/ophyd_async.panda.SeqTrigger.rst delete mode 100644 docs/user/generated/ophyd_async.panda.phase_sorter.rst delete mode 100644 docs/user/generated/ophyd_async.panda.pvi.rst delete mode 100644 docs/user/generated/ophyd_async.panda.rst delete mode 100644 docs/user/generated/ophyd_async.panda.seq_table_from_arrays.rst delete mode 100644 docs/user/generated/ophyd_async.panda.seq_table_from_rows.rst diff --git a/docs/user/generated/ophyd_async.core.AsyncStatus.rst b/docs/user/generated/ophyd_async.core.AsyncStatus.rst deleted file mode 100644 index 3ba374e3ad..0000000000 --- a/docs/user/generated/ophyd_async.core.AsyncStatus.rst +++ /dev/null @@ -1,40 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.AsyncStatus -============================= - -.. currentmodule:: ophyd_async.core - -.. autoclass:: AsyncStatus - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~AsyncStatus.add_callback - ~AsyncStatus.exception - ~AsyncStatus.watch - ~AsyncStatus.wrap - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~AsyncStatus.done - ~AsyncStatus.success - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.DetectorControl.rst b/docs/user/generated/ophyd_async.core.DetectorControl.rst deleted file mode 100644 index e223460fe9..0000000000 --- a/docs/user/generated/ophyd_async.core.DetectorControl.rst +++ /dev/null @@ -1,32 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.DetectorControl -================================= - -.. currentmodule:: ophyd_async.core - -.. autoclass:: DetectorControl - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~DetectorControl.arm - ~DetectorControl.disarm - ~DetectorControl.get_deadtime - - - - - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.DetectorGroupLogic.rst b/docs/user/generated/ophyd_async.core.DetectorGroupLogic.rst deleted file mode 100644 index 468b9fe7de..0000000000 --- a/docs/user/generated/ophyd_async.core.DetectorGroupLogic.rst +++ /dev/null @@ -1,36 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.DetectorGroupLogic -==================================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: DetectorGroupLogic - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~DetectorGroupLogic.close - ~DetectorGroupLogic.collect_asset_docs - ~DetectorGroupLogic.disarm - ~DetectorGroupLogic.ensure_armed - ~DetectorGroupLogic.hints - ~DetectorGroupLogic.open - ~DetectorGroupLogic.wait_for_index - - - - - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.DetectorTrigger.rst b/docs/user/generated/ophyd_async.core.DetectorTrigger.rst deleted file mode 100644 index 041832daf3..0000000000 --- a/docs/user/generated/ophyd_async.core.DetectorTrigger.rst +++ /dev/null @@ -1,85 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.DetectorTrigger -================================= - -.. currentmodule:: ophyd_async.core - -.. autoclass:: DetectorTrigger - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~DetectorTrigger.capitalize - ~DetectorTrigger.casefold - ~DetectorTrigger.center - ~DetectorTrigger.count - ~DetectorTrigger.encode - ~DetectorTrigger.endswith - ~DetectorTrigger.expandtabs - ~DetectorTrigger.find - ~DetectorTrigger.format - ~DetectorTrigger.format_map - ~DetectorTrigger.index - ~DetectorTrigger.isalnum - ~DetectorTrigger.isalpha - ~DetectorTrigger.isascii - ~DetectorTrigger.isdecimal - ~DetectorTrigger.isdigit - ~DetectorTrigger.isidentifier - ~DetectorTrigger.islower - ~DetectorTrigger.isnumeric - ~DetectorTrigger.isprintable - ~DetectorTrigger.isspace - ~DetectorTrigger.istitle - ~DetectorTrigger.isupper - ~DetectorTrigger.join - ~DetectorTrigger.ljust - ~DetectorTrigger.lower - ~DetectorTrigger.lstrip - ~DetectorTrigger.maketrans - ~DetectorTrigger.partition - ~DetectorTrigger.removeprefix - ~DetectorTrigger.removesuffix - ~DetectorTrigger.replace - ~DetectorTrigger.rfind - ~DetectorTrigger.rindex - ~DetectorTrigger.rjust - ~DetectorTrigger.rpartition - ~DetectorTrigger.rsplit - ~DetectorTrigger.rstrip - ~DetectorTrigger.split - ~DetectorTrigger.splitlines - ~DetectorTrigger.startswith - ~DetectorTrigger.strip - ~DetectorTrigger.swapcase - ~DetectorTrigger.title - ~DetectorTrigger.translate - ~DetectorTrigger.upper - ~DetectorTrigger.zfill - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~DetectorTrigger.internal - ~DetectorTrigger.edge_trigger - ~DetectorTrigger.constant_gate - ~DetectorTrigger.variable_gate - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.DetectorWriter.rst b/docs/user/generated/ophyd_async.core.DetectorWriter.rst deleted file mode 100644 index 77a60dc420..0000000000 --- a/docs/user/generated/ophyd_async.core.DetectorWriter.rst +++ /dev/null @@ -1,34 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.DetectorWriter -================================ - -.. currentmodule:: ophyd_async.core - -.. autoclass:: DetectorWriter - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~DetectorWriter.close - ~DetectorWriter.collect_stream_docs - ~DetectorWriter.get_indices_written - ~DetectorWriter.observe_indices_written - ~DetectorWriter.open - - - - - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.Device.rst b/docs/user/generated/ophyd_async.core.Device.rst deleted file mode 100644 index c179faff57..0000000000 --- a/docs/user/generated/ophyd_async.core.Device.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.Device -======================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: Device - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~Device.children - ~Device.connect - ~Device.set_name - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~Device.name - ~Device.parent - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.DeviceCollector.rst b/docs/user/generated/ophyd_async.core.DeviceCollector.rst deleted file mode 100644 index d69e00b14a..0000000000 --- a/docs/user/generated/ophyd_async.core.DeviceCollector.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.DeviceCollector -================================= - -.. currentmodule:: ophyd_async.core - -.. autoclass:: DeviceCollector - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - - - - - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.DeviceVector.rst b/docs/user/generated/ophyd_async.core.DeviceVector.rst deleted file mode 100644 index b781bc4c41..0000000000 --- a/docs/user/generated/ophyd_async.core.DeviceVector.rst +++ /dev/null @@ -1,50 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.DeviceVector -============================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: DeviceVector - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~DeviceVector.children - ~DeviceVector.clear - ~DeviceVector.connect - ~DeviceVector.copy - ~DeviceVector.fromkeys - ~DeviceVector.get - ~DeviceVector.items - ~DeviceVector.keys - ~DeviceVector.pop - ~DeviceVector.popitem - ~DeviceVector.set_name - ~DeviceVector.setdefault - ~DeviceVector.update - ~DeviceVector.values - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~DeviceVector.name - ~DeviceVector.parent - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.DirectoryInfo.rst b/docs/user/generated/ophyd_async.core.DirectoryInfo.rst deleted file mode 100644 index 73a6ec5208..0000000000 --- a/docs/user/generated/ophyd_async.core.DirectoryInfo.rst +++ /dev/null @@ -1,38 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.DirectoryInfo -=============================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: DirectoryInfo - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~DirectoryInfo.prefix - ~DirectoryInfo.suffix - ~DirectoryInfo.root - ~DirectoryInfo.resource_dir - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.DirectoryProvider.rst b/docs/user/generated/ophyd_async.core.DirectoryProvider.rst deleted file mode 100644 index 79ac049fec..0000000000 --- a/docs/user/generated/ophyd_async.core.DirectoryProvider.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.DirectoryProvider -=================================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: DirectoryProvider - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - - - - - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.HardwareTriggeredFlyable.rst b/docs/user/generated/ophyd_async.core.HardwareTriggeredFlyable.rst deleted file mode 100644 index 1dd7090578..0000000000 --- a/docs/user/generated/ophyd_async.core.HardwareTriggeredFlyable.rst +++ /dev/null @@ -1,48 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.HardwareTriggeredFlyable -========================================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: HardwareTriggeredFlyable - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~HardwareTriggeredFlyable.children - ~HardwareTriggeredFlyable.complete - ~HardwareTriggeredFlyable.connect - ~HardwareTriggeredFlyable.describe_configuration - ~HardwareTriggeredFlyable.kickoff - ~HardwareTriggeredFlyable.prepare - ~HardwareTriggeredFlyable.read_configuration - ~HardwareTriggeredFlyable.set_name - ~HardwareTriggeredFlyable.stage - ~HardwareTriggeredFlyable.unstage - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~HardwareTriggeredFlyable.name - ~HardwareTriggeredFlyable.parent - ~HardwareTriggeredFlyable.trigger_info - ~HardwareTriggeredFlyable.trigger_logic - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.NameProvider.rst b/docs/user/generated/ophyd_async.core.NameProvider.rst deleted file mode 100644 index c6ad3fd504..0000000000 --- a/docs/user/generated/ophyd_async.core.NameProvider.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.NameProvider -============================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: NameProvider - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - - - - - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.NotConnected.rst b/docs/user/generated/ophyd_async.core.NotConnected.rst deleted file mode 100644 index a1a87c3ecc..0000000000 --- a/docs/user/generated/ophyd_async.core.NotConnected.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.NotConnected -============================== - -.. currentmodule:: ophyd_async.core - -.. autoexception:: NotConnected \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.SameTriggerDetectorGroupLogic.rst b/docs/user/generated/ophyd_async.core.SameTriggerDetectorGroupLogic.rst deleted file mode 100644 index 4481f95c28..0000000000 --- a/docs/user/generated/ophyd_async.core.SameTriggerDetectorGroupLogic.rst +++ /dev/null @@ -1,36 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.SameTriggerDetectorGroupLogic -=============================================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: SameTriggerDetectorGroupLogic - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~SameTriggerDetectorGroupLogic.close - ~SameTriggerDetectorGroupLogic.collect_asset_docs - ~SameTriggerDetectorGroupLogic.disarm - ~SameTriggerDetectorGroupLogic.ensure_armed - ~SameTriggerDetectorGroupLogic.hints - ~SameTriggerDetectorGroupLogic.open - ~SameTriggerDetectorGroupLogic.wait_for_index - - - - - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.ShapeProvider.rst b/docs/user/generated/ophyd_async.core.ShapeProvider.rst deleted file mode 100644 index c6652b7a21..0000000000 --- a/docs/user/generated/ophyd_async.core.ShapeProvider.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.ShapeProvider -=============================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: ShapeProvider - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - - - - - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.Signal.rst b/docs/user/generated/ophyd_async.core.Signal.rst deleted file mode 100644 index 44871e3c45..0000000000 --- a/docs/user/generated/ophyd_async.core.Signal.rst +++ /dev/null @@ -1,40 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.Signal -======================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: Signal - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~Signal.children - ~Signal.connect - ~Signal.set_name - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~Signal.name - ~Signal.parent - ~Signal.source - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.SignalBackend.rst b/docs/user/generated/ophyd_async.core.SignalBackend.rst deleted file mode 100644 index eb68981078..0000000000 --- a/docs/user/generated/ophyd_async.core.SignalBackend.rst +++ /dev/null @@ -1,43 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.SignalBackend -=============================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: SignalBackend - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~SignalBackend.connect - ~SignalBackend.get_descriptor - ~SignalBackend.get_reading - ~SignalBackend.get_setpoint - ~SignalBackend.get_value - ~SignalBackend.put - ~SignalBackend.set_callback - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~SignalBackend.datatype - ~SignalBackend.source - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.SignalR.rst b/docs/user/generated/ophyd_async.core.SignalR.rst deleted file mode 100644 index dcba0f9937..0000000000 --- a/docs/user/generated/ophyd_async.core.SignalR.rst +++ /dev/null @@ -1,48 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.SignalR -========================= - -.. currentmodule:: ophyd_async.core - -.. autoclass:: SignalR - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~SignalR.children - ~SignalR.clear_sub - ~SignalR.connect - ~SignalR.describe - ~SignalR.get_value - ~SignalR.read - ~SignalR.set_name - ~SignalR.stage - ~SignalR.subscribe - ~SignalR.subscribe_value - ~SignalR.unstage - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~SignalR.name - ~SignalR.parent - ~SignalR.source - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.SignalRW.rst b/docs/user/generated/ophyd_async.core.SignalRW.rst deleted file mode 100644 index e15e55f7f4..0000000000 --- a/docs/user/generated/ophyd_async.core.SignalRW.rst +++ /dev/null @@ -1,50 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.SignalRW -========================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: SignalRW - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~SignalRW.children - ~SignalRW.clear_sub - ~SignalRW.connect - ~SignalRW.describe - ~SignalRW.get_value - ~SignalRW.locate - ~SignalRW.read - ~SignalRW.set - ~SignalRW.set_name - ~SignalRW.stage - ~SignalRW.subscribe - ~SignalRW.subscribe_value - ~SignalRW.unstage - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~SignalRW.name - ~SignalRW.parent - ~SignalRW.source - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.SignalW.rst b/docs/user/generated/ophyd_async.core.SignalW.rst deleted file mode 100644 index fff9d64b7f..0000000000 --- a/docs/user/generated/ophyd_async.core.SignalW.rst +++ /dev/null @@ -1,41 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.SignalW -========================= - -.. currentmodule:: ophyd_async.core - -.. autoclass:: SignalW - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~SignalW.children - ~SignalW.connect - ~SignalW.set - ~SignalW.set_name - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~SignalW.name - ~SignalW.parent - ~SignalW.source - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.SignalX.rst b/docs/user/generated/ophyd_async.core.SignalX.rst deleted file mode 100644 index 010f50395a..0000000000 --- a/docs/user/generated/ophyd_async.core.SignalX.rst +++ /dev/null @@ -1,41 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.SignalX -========================= - -.. currentmodule:: ophyd_async.core - -.. autoclass:: SignalX - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~SignalX.children - ~SignalX.connect - ~SignalX.set_name - ~SignalX.trigger - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~SignalX.name - ~SignalX.parent - ~SignalX.source - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.SimSignalBackend.rst b/docs/user/generated/ophyd_async.core.SimSignalBackend.rst deleted file mode 100644 index 340bd63fb0..0000000000 --- a/docs/user/generated/ophyd_async.core.SimSignalBackend.rst +++ /dev/null @@ -1,43 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.SimSignalBackend -================================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: SimSignalBackend - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~SimSignalBackend.connect - ~SimSignalBackend.get_descriptor - ~SimSignalBackend.get_reading - ~SimSignalBackend.get_setpoint - ~SimSignalBackend.get_value - ~SimSignalBackend.put - ~SimSignalBackend.set_callback - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~SimSignalBackend.datatype - ~SimSignalBackend.source - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.StandardDetector.rst b/docs/user/generated/ophyd_async.core.StandardDetector.rst deleted file mode 100644 index 7274a9a1bc..0000000000 --- a/docs/user/generated/ophyd_async.core.StandardDetector.rst +++ /dev/null @@ -1,55 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.StandardDetector -================================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: StandardDetector - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~StandardDetector.check_config_sigs - ~StandardDetector.children - ~StandardDetector.collect_asset_docs - ~StandardDetector.complete - ~StandardDetector.connect - ~StandardDetector.describe - ~StandardDetector.describe_collect - ~StandardDetector.describe_configuration - ~StandardDetector.get_index - ~StandardDetector.kickoff - ~StandardDetector.prepare - ~StandardDetector.read - ~StandardDetector.read_configuration - ~StandardDetector.set_name - ~StandardDetector.stage - ~StandardDetector.trigger - ~StandardDetector.unstage - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~StandardDetector.controller - ~StandardDetector.name - ~StandardDetector.parent - ~StandardDetector.writer - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.StandardReadable.rst b/docs/user/generated/ophyd_async.core.StandardReadable.rst deleted file mode 100644 index 9457eb4862..0000000000 --- a/docs/user/generated/ophyd_async.core.StandardReadable.rst +++ /dev/null @@ -1,46 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.StandardReadable -================================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: StandardReadable - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~StandardReadable.children - ~StandardReadable.connect - ~StandardReadable.describe - ~StandardReadable.describe_configuration - ~StandardReadable.read - ~StandardReadable.read_configuration - ~StandardReadable.set_name - ~StandardReadable.set_readable_signals - ~StandardReadable.stage - ~StandardReadable.unstage - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~StandardReadable.name - ~StandardReadable.parent - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.StaticDirectoryProvider.rst b/docs/user/generated/ophyd_async.core.StaticDirectoryProvider.rst deleted file mode 100644 index f7376a9025..0000000000 --- a/docs/user/generated/ophyd_async.core.StaticDirectoryProvider.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.StaticDirectoryProvider -========================================= - -.. currentmodule:: ophyd_async.core - -.. autoclass:: StaticDirectoryProvider - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - - - - - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.TriggerInfo.rst b/docs/user/generated/ophyd_async.core.TriggerInfo.rst deleted file mode 100644 index dfcdb2b3a7..0000000000 --- a/docs/user/generated/ophyd_async.core.TriggerInfo.rst +++ /dev/null @@ -1,38 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.TriggerInfo -============================= - -.. currentmodule:: ophyd_async.core - -.. autoclass:: TriggerInfo - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~TriggerInfo.num - ~TriggerInfo.trigger - ~TriggerInfo.deadtime - ~TriggerInfo.livetime - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.TriggerLogic.rst b/docs/user/generated/ophyd_async.core.TriggerLogic.rst deleted file mode 100644 index 32b0b44d8c..0000000000 --- a/docs/user/generated/ophyd_async.core.TriggerLogic.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core.TriggerLogic -============================== - -.. currentmodule:: ophyd_async.core - -.. autoclass:: TriggerLogic - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~TriggerLogic.prepare - ~TriggerLogic.start - ~TriggerLogic.stop - ~TriggerLogic.trigger_info - - - - - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.get_dtype.rst b/docs/user/generated/ophyd_async.core.get_dtype.rst deleted file mode 100644 index 064141c3d7..0000000000 --- a/docs/user/generated/ophyd_async.core.get_dtype.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.get\_dtype -============================ - -.. currentmodule:: ophyd_async.core - -.. autofunction:: get_dtype \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.get_signal_values.rst b/docs/user/generated/ophyd_async.core.get_signal_values.rst deleted file mode 100644 index d0431fafb0..0000000000 --- a/docs/user/generated/ophyd_async.core.get_signal_values.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.get\_signal\_values -===================================== - -.. currentmodule:: ophyd_async.core - -.. autofunction:: get_signal_values \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.get_unique.rst b/docs/user/generated/ophyd_async.core.get_unique.rst deleted file mode 100644 index e40e8afc0a..0000000000 --- a/docs/user/generated/ophyd_async.core.get_unique.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.get\_unique -============================= - -.. currentmodule:: ophyd_async.core - -.. autofunction:: get_unique \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.load_device.rst b/docs/user/generated/ophyd_async.core.load_device.rst deleted file mode 100644 index 33c50b390c..0000000000 --- a/docs/user/generated/ophyd_async.core.load_device.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.load\_device -============================== - -.. currentmodule:: ophyd_async.core - -.. autofunction:: load_device \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.load_from_yaml.rst b/docs/user/generated/ophyd_async.core.load_from_yaml.rst deleted file mode 100644 index 50ae1757c9..0000000000 --- a/docs/user/generated/ophyd_async.core.load_from_yaml.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.load\_from\_yaml -================================== - -.. currentmodule:: ophyd_async.core - -.. autofunction:: load_from_yaml \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.merge_gathered_dicts.rst b/docs/user/generated/ophyd_async.core.merge_gathered_dicts.rst deleted file mode 100644 index 319e7f6d02..0000000000 --- a/docs/user/generated/ophyd_async.core.merge_gathered_dicts.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.merge\_gathered\_dicts -======================================== - -.. currentmodule:: ophyd_async.core - -.. autofunction:: merge_gathered_dicts \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.observe_value.rst b/docs/user/generated/ophyd_async.core.observe_value.rst deleted file mode 100644 index a6cad4be28..0000000000 --- a/docs/user/generated/ophyd_async.core.observe_value.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.observe\_value -================================ - -.. currentmodule:: ophyd_async.core - -.. autofunction:: observe_value \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.rst b/docs/user/generated/ophyd_async.core.rst deleted file mode 100644 index 4cf15eec62..0000000000 --- a/docs/user/generated/ophyd_async.core.rst +++ /dev/null @@ -1,92 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.core -================= - -.. automodule:: ophyd_async.core - - - - - - - - .. rubric:: Functions - - .. autosummary:: - :toctree: - :nosignatures: - - observe_value - set_and_wait_for_value - set_sim_callback - set_sim_put_proceeds - set_sim_value - wait_for_value - get_dtype - get_unique - merge_gathered_dicts - wait_for_connection - get_signal_values - load_from_yaml - save_to_yaml - set_signal_values - walk_rw_signals - load_device - save_device - - - - - - .. rubric:: Classes - - .. autosummary:: - :toctree: - :template: custom-class-template.rst - :nosignatures: - - SignalBackend - SimSignalBackend - DetectorControl - DetectorTrigger - DetectorWriter - StandardDetector - Device - DeviceCollector - DeviceVector - Signal - SignalR - SignalW - SignalRW - SignalX - AsyncStatus - DirectoryInfo - DirectoryProvider - NameProvider - ShapeProvider - StaticDirectoryProvider - StandardReadable - TriggerInfo - TriggerLogic - HardwareTriggeredFlyable - - - - - - .. rubric:: Exceptions - - .. autosummary:: - :toctree: - :nosignatures: - - NotConnected - - - - - diff --git a/docs/user/generated/ophyd_async.core.save_device.rst b/docs/user/generated/ophyd_async.core.save_device.rst deleted file mode 100644 index cbd2a994a3..0000000000 --- a/docs/user/generated/ophyd_async.core.save_device.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.save\_device -============================== - -.. currentmodule:: ophyd_async.core - -.. autofunction:: save_device \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.save_to_yaml.rst b/docs/user/generated/ophyd_async.core.save_to_yaml.rst deleted file mode 100644 index cf4b995afc..0000000000 --- a/docs/user/generated/ophyd_async.core.save_to_yaml.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.save\_to\_yaml -================================ - -.. currentmodule:: ophyd_async.core - -.. autofunction:: save_to_yaml \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.set_and_wait_for_value.rst b/docs/user/generated/ophyd_async.core.set_and_wait_for_value.rst deleted file mode 100644 index d6a1d47575..0000000000 --- a/docs/user/generated/ophyd_async.core.set_and_wait_for_value.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.set\_and\_wait\_for\_value -============================================ - -.. currentmodule:: ophyd_async.core - -.. autofunction:: set_and_wait_for_value \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.set_signal_values.rst b/docs/user/generated/ophyd_async.core.set_signal_values.rst deleted file mode 100644 index 2c75f73bcb..0000000000 --- a/docs/user/generated/ophyd_async.core.set_signal_values.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.set\_signal\_values -===================================== - -.. currentmodule:: ophyd_async.core - -.. autofunction:: set_signal_values \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.set_sim_callback.rst b/docs/user/generated/ophyd_async.core.set_sim_callback.rst deleted file mode 100644 index 7b92826650..0000000000 --- a/docs/user/generated/ophyd_async.core.set_sim_callback.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.set\_sim\_callback -==================================== - -.. currentmodule:: ophyd_async.core - -.. autofunction:: set_sim_callback \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.set_sim_put_proceeds.rst b/docs/user/generated/ophyd_async.core.set_sim_put_proceeds.rst deleted file mode 100644 index 1da7f46bc5..0000000000 --- a/docs/user/generated/ophyd_async.core.set_sim_put_proceeds.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.set\_sim\_put\_proceeds -========================================= - -.. currentmodule:: ophyd_async.core - -.. autofunction:: set_sim_put_proceeds \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.set_sim_value.rst b/docs/user/generated/ophyd_async.core.set_sim_value.rst deleted file mode 100644 index 6d61ef1739..0000000000 --- a/docs/user/generated/ophyd_async.core.set_sim_value.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.set\_sim\_value -================================= - -.. currentmodule:: ophyd_async.core - -.. autofunction:: set_sim_value \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.wait_for_connection.rst b/docs/user/generated/ophyd_async.core.wait_for_connection.rst deleted file mode 100644 index 6ce52ae8f7..0000000000 --- a/docs/user/generated/ophyd_async.core.wait_for_connection.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.wait\_for\_connection -======================================= - -.. currentmodule:: ophyd_async.core - -.. autofunction:: wait_for_connection \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.wait_for_value.rst b/docs/user/generated/ophyd_async.core.wait_for_value.rst deleted file mode 100644 index 1714d55d57..0000000000 --- a/docs/user/generated/ophyd_async.core.wait_for_value.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.wait\_for\_value -================================== - -.. currentmodule:: ophyd_async.core - -.. autofunction:: wait_for_value \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.core.walk_rw_signals.rst b/docs/user/generated/ophyd_async.core.walk_rw_signals.rst deleted file mode 100644 index 3182dcb601..0000000000 --- a/docs/user/generated/ophyd_async.core.walk_rw_signals.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.core.walk\_rw\_signals -=================================== - -.. currentmodule:: ophyd_async.core - -.. autofunction:: walk_rw_signals \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.areadetector.FileWriteMode.rst b/docs/user/generated/ophyd_async.epics.areadetector.FileWriteMode.rst deleted file mode 100644 index 796590342d..0000000000 --- a/docs/user/generated/ophyd_async.epics.areadetector.FileWriteMode.rst +++ /dev/null @@ -1,84 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.areadetector.FileWriteMode -============================================= - -.. currentmodule:: ophyd_async.epics.areadetector - -.. autoclass:: FileWriteMode - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~FileWriteMode.capitalize - ~FileWriteMode.casefold - ~FileWriteMode.center - ~FileWriteMode.count - ~FileWriteMode.encode - ~FileWriteMode.endswith - ~FileWriteMode.expandtabs - ~FileWriteMode.find - ~FileWriteMode.format - ~FileWriteMode.format_map - ~FileWriteMode.index - ~FileWriteMode.isalnum - ~FileWriteMode.isalpha - ~FileWriteMode.isascii - ~FileWriteMode.isdecimal - ~FileWriteMode.isdigit - ~FileWriteMode.isidentifier - ~FileWriteMode.islower - ~FileWriteMode.isnumeric - ~FileWriteMode.isprintable - ~FileWriteMode.isspace - ~FileWriteMode.istitle - ~FileWriteMode.isupper - ~FileWriteMode.join - ~FileWriteMode.ljust - ~FileWriteMode.lower - ~FileWriteMode.lstrip - ~FileWriteMode.maketrans - ~FileWriteMode.partition - ~FileWriteMode.removeprefix - ~FileWriteMode.removesuffix - ~FileWriteMode.replace - ~FileWriteMode.rfind - ~FileWriteMode.rindex - ~FileWriteMode.rjust - ~FileWriteMode.rpartition - ~FileWriteMode.rsplit - ~FileWriteMode.rstrip - ~FileWriteMode.split - ~FileWriteMode.splitlines - ~FileWriteMode.startswith - ~FileWriteMode.strip - ~FileWriteMode.swapcase - ~FileWriteMode.title - ~FileWriteMode.translate - ~FileWriteMode.upper - ~FileWriteMode.zfill - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~FileWriteMode.single - ~FileWriteMode.capture - ~FileWriteMode.stream - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.areadetector.ImageMode.rst b/docs/user/generated/ophyd_async.epics.areadetector.ImageMode.rst deleted file mode 100644 index c4a1251546..0000000000 --- a/docs/user/generated/ophyd_async.epics.areadetector.ImageMode.rst +++ /dev/null @@ -1,84 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.areadetector.ImageMode -========================================= - -.. currentmodule:: ophyd_async.epics.areadetector - -.. autoclass:: ImageMode - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~ImageMode.capitalize - ~ImageMode.casefold - ~ImageMode.center - ~ImageMode.count - ~ImageMode.encode - ~ImageMode.endswith - ~ImageMode.expandtabs - ~ImageMode.find - ~ImageMode.format - ~ImageMode.format_map - ~ImageMode.index - ~ImageMode.isalnum - ~ImageMode.isalpha - ~ImageMode.isascii - ~ImageMode.isdecimal - ~ImageMode.isdigit - ~ImageMode.isidentifier - ~ImageMode.islower - ~ImageMode.isnumeric - ~ImageMode.isprintable - ~ImageMode.isspace - ~ImageMode.istitle - ~ImageMode.isupper - ~ImageMode.join - ~ImageMode.ljust - ~ImageMode.lower - ~ImageMode.lstrip - ~ImageMode.maketrans - ~ImageMode.partition - ~ImageMode.removeprefix - ~ImageMode.removesuffix - ~ImageMode.replace - ~ImageMode.rfind - ~ImageMode.rindex - ~ImageMode.rjust - ~ImageMode.rpartition - ~ImageMode.rsplit - ~ImageMode.rstrip - ~ImageMode.split - ~ImageMode.splitlines - ~ImageMode.startswith - ~ImageMode.strip - ~ImageMode.swapcase - ~ImageMode.title - ~ImageMode.translate - ~ImageMode.upper - ~ImageMode.zfill - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~ImageMode.single - ~ImageMode.multiple - ~ImageMode.continuous - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.areadetector.NDAttributeDataType.rst b/docs/user/generated/ophyd_async.epics.areadetector.NDAttributeDataType.rst deleted file mode 100644 index 1ec149c704..0000000000 --- a/docs/user/generated/ophyd_async.epics.areadetector.NDAttributeDataType.rst +++ /dev/null @@ -1,84 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.areadetector.NDAttributeDataType -=================================================== - -.. currentmodule:: ophyd_async.epics.areadetector - -.. autoclass:: NDAttributeDataType - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~NDAttributeDataType.capitalize - ~NDAttributeDataType.casefold - ~NDAttributeDataType.center - ~NDAttributeDataType.count - ~NDAttributeDataType.encode - ~NDAttributeDataType.endswith - ~NDAttributeDataType.expandtabs - ~NDAttributeDataType.find - ~NDAttributeDataType.format - ~NDAttributeDataType.format_map - ~NDAttributeDataType.index - ~NDAttributeDataType.isalnum - ~NDAttributeDataType.isalpha - ~NDAttributeDataType.isascii - ~NDAttributeDataType.isdecimal - ~NDAttributeDataType.isdigit - ~NDAttributeDataType.isidentifier - ~NDAttributeDataType.islower - ~NDAttributeDataType.isnumeric - ~NDAttributeDataType.isprintable - ~NDAttributeDataType.isspace - ~NDAttributeDataType.istitle - ~NDAttributeDataType.isupper - ~NDAttributeDataType.join - ~NDAttributeDataType.ljust - ~NDAttributeDataType.lower - ~NDAttributeDataType.lstrip - ~NDAttributeDataType.maketrans - ~NDAttributeDataType.partition - ~NDAttributeDataType.removeprefix - ~NDAttributeDataType.removesuffix - ~NDAttributeDataType.replace - ~NDAttributeDataType.rfind - ~NDAttributeDataType.rindex - ~NDAttributeDataType.rjust - ~NDAttributeDataType.rpartition - ~NDAttributeDataType.rsplit - ~NDAttributeDataType.rstrip - ~NDAttributeDataType.split - ~NDAttributeDataType.splitlines - ~NDAttributeDataType.startswith - ~NDAttributeDataType.strip - ~NDAttributeDataType.swapcase - ~NDAttributeDataType.title - ~NDAttributeDataType.translate - ~NDAttributeDataType.upper - ~NDAttributeDataType.zfill - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~NDAttributeDataType.INT - ~NDAttributeDataType.DOUBLE - ~NDAttributeDataType.STRING - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.areadetector.NDAttributesXML.rst b/docs/user/generated/ophyd_async.epics.areadetector.NDAttributesXML.rst deleted file mode 100644 index 0b64168516..0000000000 --- a/docs/user/generated/ophyd_async.epics.areadetector.NDAttributesXML.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.areadetector.NDAttributesXML -=============================================== - -.. currentmodule:: ophyd_async.epics.areadetector - -.. autoclass:: NDAttributesXML - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~NDAttributesXML.add_epics_pv - ~NDAttributesXML.add_param - - - - - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.areadetector.SingleTriggerDet.rst b/docs/user/generated/ophyd_async.epics.areadetector.SingleTriggerDet.rst deleted file mode 100644 index 9f5a8f6465..0000000000 --- a/docs/user/generated/ophyd_async.epics.areadetector.SingleTriggerDet.rst +++ /dev/null @@ -1,47 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.areadetector.SingleTriggerDet -================================================ - -.. currentmodule:: ophyd_async.epics.areadetector - -.. autoclass:: SingleTriggerDet - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~SingleTriggerDet.children - ~SingleTriggerDet.connect - ~SingleTriggerDet.describe - ~SingleTriggerDet.describe_configuration - ~SingleTriggerDet.read - ~SingleTriggerDet.read_configuration - ~SingleTriggerDet.set_name - ~SingleTriggerDet.set_readable_signals - ~SingleTriggerDet.stage - ~SingleTriggerDet.trigger - ~SingleTriggerDet.unstage - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~SingleTriggerDet.name - ~SingleTriggerDet.parent - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.areadetector.ad_r.rst b/docs/user/generated/ophyd_async.epics.areadetector.ad_r.rst deleted file mode 100644 index 226bea1f25..0000000000 --- a/docs/user/generated/ophyd_async.epics.areadetector.ad_r.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.epics.areadetector.ad\_r -===================================== - -.. currentmodule:: ophyd_async.epics.areadetector - -.. autofunction:: ad_r \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.areadetector.ad_rw.rst b/docs/user/generated/ophyd_async.epics.areadetector.ad_rw.rst deleted file mode 100644 index d4efee2732..0000000000 --- a/docs/user/generated/ophyd_async.epics.areadetector.ad_rw.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.epics.areadetector.ad\_rw -====================================== - -.. currentmodule:: ophyd_async.epics.areadetector - -.. autofunction:: ad_rw \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.areadetector.rst b/docs/user/generated/ophyd_async.epics.areadetector.rst deleted file mode 100644 index 99c194d94b..0000000000 --- a/docs/user/generated/ophyd_async.epics.areadetector.rst +++ /dev/null @@ -1,50 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.areadetector -=============================== - -.. automodule:: ophyd_async.epics.areadetector - - - - - - - - .. rubric:: Functions - - .. autosummary:: - :toctree: - :nosignatures: - - ad_r - ad_rw - - - - - - .. rubric:: Classes - - .. autosummary:: - :toctree: - :template: custom-class-template.rst - :nosignatures: - - SingleTriggerDet - FileWriteMode - ImageMode - NDAttributeDataType - NDAttributesXML - - - - - - - - - diff --git a/docs/user/generated/ophyd_async.epics.demo.EnergyMode.rst b/docs/user/generated/ophyd_async.epics.demo.EnergyMode.rst deleted file mode 100644 index 4d2475f4ba..0000000000 --- a/docs/user/generated/ophyd_async.epics.demo.EnergyMode.rst +++ /dev/null @@ -1,83 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.demo.EnergyMode -================================== - -.. currentmodule:: ophyd_async.epics.demo - -.. autoclass:: EnergyMode - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~EnergyMode.capitalize - ~EnergyMode.casefold - ~EnergyMode.center - ~EnergyMode.count - ~EnergyMode.encode - ~EnergyMode.endswith - ~EnergyMode.expandtabs - ~EnergyMode.find - ~EnergyMode.format - ~EnergyMode.format_map - ~EnergyMode.index - ~EnergyMode.isalnum - ~EnergyMode.isalpha - ~EnergyMode.isascii - ~EnergyMode.isdecimal - ~EnergyMode.isdigit - ~EnergyMode.isidentifier - ~EnergyMode.islower - ~EnergyMode.isnumeric - ~EnergyMode.isprintable - ~EnergyMode.isspace - ~EnergyMode.istitle - ~EnergyMode.isupper - ~EnergyMode.join - ~EnergyMode.ljust - ~EnergyMode.lower - ~EnergyMode.lstrip - ~EnergyMode.maketrans - ~EnergyMode.partition - ~EnergyMode.removeprefix - ~EnergyMode.removesuffix - ~EnergyMode.replace - ~EnergyMode.rfind - ~EnergyMode.rindex - ~EnergyMode.rjust - ~EnergyMode.rpartition - ~EnergyMode.rsplit - ~EnergyMode.rstrip - ~EnergyMode.split - ~EnergyMode.splitlines - ~EnergyMode.startswith - ~EnergyMode.strip - ~EnergyMode.swapcase - ~EnergyMode.title - ~EnergyMode.translate - ~EnergyMode.upper - ~EnergyMode.zfill - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~EnergyMode.low - ~EnergyMode.high - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.demo.Mover.rst b/docs/user/generated/ophyd_async.epics.demo.Mover.rst deleted file mode 100644 index ad46b8c07e..0000000000 --- a/docs/user/generated/ophyd_async.epics.demo.Mover.rst +++ /dev/null @@ -1,49 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.demo.Mover -============================= - -.. currentmodule:: ophyd_async.epics.demo - -.. autoclass:: Mover - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~Mover.children - ~Mover.connect - ~Mover.describe - ~Mover.describe_configuration - ~Mover.move - ~Mover.read - ~Mover.read_configuration - ~Mover.set - ~Mover.set_name - ~Mover.set_readable_signals - ~Mover.stage - ~Mover.stop - ~Mover.unstage - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~Mover.name - ~Mover.parent - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.demo.SampleStage.rst b/docs/user/generated/ophyd_async.epics.demo.SampleStage.rst deleted file mode 100644 index 994598be77..0000000000 --- a/docs/user/generated/ophyd_async.epics.demo.SampleStage.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.demo.SampleStage -=================================== - -.. currentmodule:: ophyd_async.epics.demo - -.. autoclass:: SampleStage - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~SampleStage.children - ~SampleStage.connect - ~SampleStage.set_name - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~SampleStage.name - ~SampleStage.parent - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.demo.Sensor.rst b/docs/user/generated/ophyd_async.epics.demo.Sensor.rst deleted file mode 100644 index 9535179baa..0000000000 --- a/docs/user/generated/ophyd_async.epics.demo.Sensor.rst +++ /dev/null @@ -1,46 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.demo.Sensor -============================== - -.. currentmodule:: ophyd_async.epics.demo - -.. autoclass:: Sensor - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~Sensor.children - ~Sensor.connect - ~Sensor.describe - ~Sensor.describe_configuration - ~Sensor.read - ~Sensor.read_configuration - ~Sensor.set_name - ~Sensor.set_readable_signals - ~Sensor.stage - ~Sensor.unstage - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~Sensor.name - ~Sensor.parent - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.demo.demo_ad_sim_detector.DemoADSimDetector.rst b/docs/user/generated/ophyd_async.epics.demo.demo_ad_sim_detector.DemoADSimDetector.rst deleted file mode 100644 index 44e665dabb..0000000000 --- a/docs/user/generated/ophyd_async.epics.demo.demo_ad_sim_detector.DemoADSimDetector.rst +++ /dev/null @@ -1,55 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.demo.demo\_ad\_sim\_detector.DemoADSimDetector -================================================================= - -.. currentmodule:: ophyd_async.epics.demo.demo_ad_sim_detector - -.. autoclass:: DemoADSimDetector - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~DemoADSimDetector.check_config_sigs - ~DemoADSimDetector.children - ~DemoADSimDetector.collect_asset_docs - ~DemoADSimDetector.complete - ~DemoADSimDetector.connect - ~DemoADSimDetector.describe - ~DemoADSimDetector.describe_collect - ~DemoADSimDetector.describe_configuration - ~DemoADSimDetector.get_index - ~DemoADSimDetector.kickoff - ~DemoADSimDetector.prepare - ~DemoADSimDetector.read - ~DemoADSimDetector.read_configuration - ~DemoADSimDetector.set_name - ~DemoADSimDetector.stage - ~DemoADSimDetector.trigger - ~DemoADSimDetector.unstage - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~DemoADSimDetector.controller - ~DemoADSimDetector.name - ~DemoADSimDetector.parent - ~DemoADSimDetector.writer - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.demo.demo_ad_sim_detector.rst b/docs/user/generated/ophyd_async.epics.demo.demo_ad_sim_detector.rst deleted file mode 100644 index f7da2aabdc..0000000000 --- a/docs/user/generated/ophyd_async.epics.demo.demo_ad_sim_detector.rst +++ /dev/null @@ -1,37 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.demo.demo\_ad\_sim\_detector -=============================================== - -.. automodule:: ophyd_async.epics.demo.demo_ad_sim_detector - - - - - - - - - - - - .. rubric:: Classes - - .. autosummary:: - :toctree: - :template: custom-class-template.rst - :nosignatures: - - DemoADSimDetector - - - - - - - - - diff --git a/docs/user/generated/ophyd_async.epics.demo.rst b/docs/user/generated/ophyd_async.epics.demo.rst deleted file mode 100644 index e479c8be31..0000000000 --- a/docs/user/generated/ophyd_async.epics.demo.rst +++ /dev/null @@ -1,55 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.demo -======================= - -.. automodule:: ophyd_async.epics.demo - - - - - - - - .. rubric:: Functions - - .. autosummary:: - :toctree: - :nosignatures: - - start_ioc_subprocess - - - - - - .. rubric:: Classes - - .. autosummary:: - :toctree: - :template: custom-class-template.rst - :nosignatures: - - EnergyMode - Mover - SampleStage - Sensor - - - - - - - - - -.. autosummary:: - :toctree: - :template: custom-module-template.rst - :recursive: - - ophyd_async.epics.demo.demo_ad_sim_detector - diff --git a/docs/user/generated/ophyd_async.epics.demo.start_ioc_subprocess.rst b/docs/user/generated/ophyd_async.epics.demo.start_ioc_subprocess.rst deleted file mode 100644 index f30feddc9b..0000000000 --- a/docs/user/generated/ophyd_async.epics.demo.start_ioc_subprocess.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.epics.demo.start\_ioc\_subprocess -============================================== - -.. currentmodule:: ophyd_async.epics.demo - -.. autofunction:: start_ioc_subprocess \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.motion.Motor.rst b/docs/user/generated/ophyd_async.epics.motion.Motor.rst deleted file mode 100644 index aa38c9e9cd..0000000000 --- a/docs/user/generated/ophyd_async.epics.motion.Motor.rst +++ /dev/null @@ -1,49 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.motion.Motor -=============================== - -.. currentmodule:: ophyd_async.epics.motion - -.. autoclass:: Motor - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~Motor.children - ~Motor.connect - ~Motor.describe - ~Motor.describe_configuration - ~Motor.move - ~Motor.read - ~Motor.read_configuration - ~Motor.set - ~Motor.set_name - ~Motor.set_readable_signals - ~Motor.stage - ~Motor.stop - ~Motor.unstage - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~Motor.name - ~Motor.parent - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.motion.rst b/docs/user/generated/ophyd_async.epics.motion.rst deleted file mode 100644 index 4001309074..0000000000 --- a/docs/user/generated/ophyd_async.epics.motion.rst +++ /dev/null @@ -1,37 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.motion -========================= - -.. automodule:: ophyd_async.epics.motion - - - - - - - - - - - - .. rubric:: Classes - - .. autosummary:: - :toctree: - :template: custom-class-template.rst - :nosignatures: - - Motor - - - - - - - - - diff --git a/docs/user/generated/ophyd_async.epics.pvi.PVIEntry.rst b/docs/user/generated/ophyd_async.epics.pvi.PVIEntry.rst deleted file mode 100644 index 6f2e62eb17..0000000000 --- a/docs/user/generated/ophyd_async.epics.pvi.PVIEntry.rst +++ /dev/null @@ -1,50 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.pvi.PVIEntry -=============================== - -.. currentmodule:: ophyd_async.epics.pvi - -.. autoclass:: PVIEntry - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~PVIEntry.clear - ~PVIEntry.copy - ~PVIEntry.fromkeys - ~PVIEntry.get - ~PVIEntry.items - ~PVIEntry.keys - ~PVIEntry.pop - ~PVIEntry.popitem - ~PVIEntry.setdefault - ~PVIEntry.update - ~PVIEntry.values - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~PVIEntry.d - ~PVIEntry.r - ~PVIEntry.rw - ~PVIEntry.w - ~PVIEntry.x - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.pvi.make_signal.rst b/docs/user/generated/ophyd_async.epics.pvi.make_signal.rst deleted file mode 100644 index 42083c6efd..0000000000 --- a/docs/user/generated/ophyd_async.epics.pvi.make_signal.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.epics.pvi.make\_signal -=================================== - -.. currentmodule:: ophyd_async.epics.pvi - -.. autofunction:: make_signal \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.pvi.pvi_get.rst b/docs/user/generated/ophyd_async.epics.pvi.pvi_get.rst deleted file mode 100644 index feb61e7e70..0000000000 --- a/docs/user/generated/ophyd_async.epics.pvi.pvi_get.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.epics.pvi.pvi\_get -=============================== - -.. currentmodule:: ophyd_async.epics.pvi - -.. autofunction:: pvi_get \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.pvi.rst b/docs/user/generated/ophyd_async.epics.pvi.rst deleted file mode 100644 index dd626f05b1..0000000000 --- a/docs/user/generated/ophyd_async.epics.pvi.rst +++ /dev/null @@ -1,46 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.pvi -====================== - -.. automodule:: ophyd_async.epics.pvi - - - - - - - - .. rubric:: Functions - - .. autosummary:: - :toctree: - :nosignatures: - - make_signal - pvi_get - - - - - - .. rubric:: Classes - - .. autosummary:: - :toctree: - :template: custom-class-template.rst - :nosignatures: - - PVIEntry - - - - - - - - - diff --git a/docs/user/generated/ophyd_async.epics.rst b/docs/user/generated/ophyd_async.epics.rst deleted file mode 100644 index 8e262ec1e4..0000000000 --- a/docs/user/generated/ophyd_async.epics.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics -================== - -.. automodule:: ophyd_async.epics - - - - - - - - - - - - - - - - - - - -.. autosummary:: - :toctree: - :template: custom-module-template.rst - :recursive: - - ophyd_async.epics.areadetector - ophyd_async.epics.demo - ophyd_async.epics.motion - ophyd_async.epics.pvi - ophyd_async.epics.signal - diff --git a/docs/user/generated/ophyd_async.epics.signal.epics_signal_r.rst b/docs/user/generated/ophyd_async.epics.signal.epics_signal_r.rst deleted file mode 100644 index 6ef942d039..0000000000 --- a/docs/user/generated/ophyd_async.epics.signal.epics_signal_r.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.epics.signal.epics\_signal\_r -========================================== - -.. currentmodule:: ophyd_async.epics.signal - -.. autofunction:: epics_signal_r \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.signal.epics_signal_rw.rst b/docs/user/generated/ophyd_async.epics.signal.epics_signal_rw.rst deleted file mode 100644 index 14893c8089..0000000000 --- a/docs/user/generated/ophyd_async.epics.signal.epics_signal_rw.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.epics.signal.epics\_signal\_rw -=========================================== - -.. currentmodule:: ophyd_async.epics.signal - -.. autofunction:: epics_signal_rw \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.signal.epics_signal_w.rst b/docs/user/generated/ophyd_async.epics.signal.epics_signal_w.rst deleted file mode 100644 index 364875af3a..0000000000 --- a/docs/user/generated/ophyd_async.epics.signal.epics_signal_w.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.epics.signal.epics\_signal\_w -========================================== - -.. currentmodule:: ophyd_async.epics.signal - -.. autofunction:: epics_signal_w \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.signal.epics_signal_x.rst b/docs/user/generated/ophyd_async.epics.signal.epics_signal_x.rst deleted file mode 100644 index e6b22de1ad..0000000000 --- a/docs/user/generated/ophyd_async.epics.signal.epics_signal_x.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.epics.signal.epics\_signal\_x -========================================== - -.. currentmodule:: ophyd_async.epics.signal - -.. autofunction:: epics_signal_x \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.signal.pvi_get.rst b/docs/user/generated/ophyd_async.epics.signal.pvi_get.rst deleted file mode 100644 index 7d0b1bccab..0000000000 --- a/docs/user/generated/ophyd_async.epics.signal.pvi_get.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.epics.signal.pvi\_get -================================== - -.. currentmodule:: ophyd_async.epics.signal - -.. autofunction:: pvi_get \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.epics.signal.rst b/docs/user/generated/ophyd_async.epics.signal.rst deleted file mode 100644 index 14bb2a50ca..0000000000 --- a/docs/user/generated/ophyd_async.epics.signal.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.epics.signal -========================= - -.. automodule:: ophyd_async.epics.signal - - - - - - - - .. rubric:: Functions - - .. autosummary:: - :toctree: - :nosignatures: - - epics_signal_r - epics_signal_rw - epics_signal_w - epics_signal_x - - - - - - - - - - - - - diff --git a/docs/user/generated/ophyd_async.panda.PVIEntry.rst b/docs/user/generated/ophyd_async.panda.PVIEntry.rst deleted file mode 100644 index 3e557f622a..0000000000 --- a/docs/user/generated/ophyd_async.panda.PVIEntry.rst +++ /dev/null @@ -1,50 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.panda.PVIEntry -=========================== - -.. currentmodule:: ophyd_async.panda - -.. autoclass:: PVIEntry - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~PVIEntry.clear - ~PVIEntry.copy - ~PVIEntry.fromkeys - ~PVIEntry.get - ~PVIEntry.items - ~PVIEntry.keys - ~PVIEntry.pop - ~PVIEntry.popitem - ~PVIEntry.setdefault - ~PVIEntry.update - ~PVIEntry.values - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~PVIEntry.d - ~PVIEntry.r - ~PVIEntry.rw - ~PVIEntry.w - ~PVIEntry.x - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.panda.PandA.rst b/docs/user/generated/ophyd_async.panda.PandA.rst deleted file mode 100644 index 84590dc633..0000000000 --- a/docs/user/generated/ophyd_async.panda.PandA.rst +++ /dev/null @@ -1,44 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.panda.PandA -======================== - -.. currentmodule:: ophyd_async.panda - -.. autoclass:: PandA - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~PandA.children - ~PandA.connect - ~PandA.set_attribute - ~PandA.set_name - ~PandA.verify_block - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~PandA.name - ~PandA.parent - ~PandA.pulse - ~PandA.seq - ~PandA.pcap - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.panda.PandaPcapController.rst b/docs/user/generated/ophyd_async.panda.PandaPcapController.rst deleted file mode 100644 index a7842f1749..0000000000 --- a/docs/user/generated/ophyd_async.panda.PandaPcapController.rst +++ /dev/null @@ -1,32 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.panda.PandaPcapController -====================================== - -.. currentmodule:: ophyd_async.panda - -.. autoclass:: PandaPcapController - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~PandaPcapController.arm - ~PandaPcapController.disarm - ~PandaPcapController.get_deadtime - - - - - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.panda.PcapBlock.rst b/docs/user/generated/ophyd_async.panda.PcapBlock.rst deleted file mode 100644 index c6f0b17a8f..0000000000 --- a/docs/user/generated/ophyd_async.panda.PcapBlock.rst +++ /dev/null @@ -1,41 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.panda.PcapBlock -============================ - -.. currentmodule:: ophyd_async.panda - -.. autoclass:: PcapBlock - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~PcapBlock.children - ~PcapBlock.connect - ~PcapBlock.set_name - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~PcapBlock.name - ~PcapBlock.parent - ~PcapBlock.active - ~PcapBlock.arm - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.panda.PulseBlock.rst b/docs/user/generated/ophyd_async.panda.PulseBlock.rst deleted file mode 100644 index 27c8fcb13a..0000000000 --- a/docs/user/generated/ophyd_async.panda.PulseBlock.rst +++ /dev/null @@ -1,41 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.panda.PulseBlock -============================= - -.. currentmodule:: ophyd_async.panda - -.. autoclass:: PulseBlock - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~PulseBlock.children - ~PulseBlock.connect - ~PulseBlock.set_name - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~PulseBlock.name - ~PulseBlock.parent - ~PulseBlock.delay - ~PulseBlock.width - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.panda.SeqBlock.rst b/docs/user/generated/ophyd_async.panda.SeqBlock.rst deleted file mode 100644 index 032ab8290d..0000000000 --- a/docs/user/generated/ophyd_async.panda.SeqBlock.rst +++ /dev/null @@ -1,41 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.panda.SeqBlock -=========================== - -.. currentmodule:: ophyd_async.panda - -.. autoclass:: SeqBlock - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~SeqBlock.children - ~SeqBlock.connect - ~SeqBlock.set_name - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~SeqBlock.name - ~SeqBlock.parent - ~SeqBlock.table - ~SeqBlock.active - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.panda.SeqTable.rst b/docs/user/generated/ophyd_async.panda.SeqTable.rst deleted file mode 100644 index c5fecbde05..0000000000 --- a/docs/user/generated/ophyd_async.panda.SeqTable.rst +++ /dev/null @@ -1,62 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.panda.SeqTable -=========================== - -.. currentmodule:: ophyd_async.panda - -.. autoclass:: SeqTable - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~SeqTable.clear - ~SeqTable.copy - ~SeqTable.fromkeys - ~SeqTable.get - ~SeqTable.items - ~SeqTable.keys - ~SeqTable.pop - ~SeqTable.popitem - ~SeqTable.setdefault - ~SeqTable.update - ~SeqTable.values - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~SeqTable.repeats - ~SeqTable.trigger - ~SeqTable.position - ~SeqTable.time1 - ~SeqTable.outa1 - ~SeqTable.outb1 - ~SeqTable.outc1 - ~SeqTable.outd1 - ~SeqTable.oute1 - ~SeqTable.outf1 - ~SeqTable.time2 - ~SeqTable.outa2 - ~SeqTable.outb2 - ~SeqTable.outc2 - ~SeqTable.outd2 - ~SeqTable.oute2 - ~SeqTable.outf2 - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.panda.SeqTableRow.rst b/docs/user/generated/ophyd_async.panda.SeqTableRow.rst deleted file mode 100644 index 77bea09578..0000000000 --- a/docs/user/generated/ophyd_async.panda.SeqTableRow.rst +++ /dev/null @@ -1,51 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.panda.SeqTableRow -============================== - -.. currentmodule:: ophyd_async.panda - -.. autoclass:: SeqTableRow - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~SeqTableRow.outa1 - ~SeqTableRow.outa2 - ~SeqTableRow.outb1 - ~SeqTableRow.outb2 - ~SeqTableRow.outc1 - ~SeqTableRow.outc2 - ~SeqTableRow.outd1 - ~SeqTableRow.outd2 - ~SeqTableRow.oute1 - ~SeqTableRow.oute2 - ~SeqTableRow.outf1 - ~SeqTableRow.outf2 - ~SeqTableRow.position - ~SeqTableRow.repeats - ~SeqTableRow.time1 - ~SeqTableRow.time2 - ~SeqTableRow.trigger - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.panda.SeqTrigger.rst b/docs/user/generated/ophyd_async.panda.SeqTrigger.rst deleted file mode 100644 index dacffde214..0000000000 --- a/docs/user/generated/ophyd_async.panda.SeqTrigger.rst +++ /dev/null @@ -1,94 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.panda.SeqTrigger -============================= - -.. currentmodule:: ophyd_async.panda - -.. autoclass:: SeqTrigger - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~SeqTrigger.capitalize - ~SeqTrigger.casefold - ~SeqTrigger.center - ~SeqTrigger.count - ~SeqTrigger.encode - ~SeqTrigger.endswith - ~SeqTrigger.expandtabs - ~SeqTrigger.find - ~SeqTrigger.format - ~SeqTrigger.format_map - ~SeqTrigger.index - ~SeqTrigger.isalnum - ~SeqTrigger.isalpha - ~SeqTrigger.isascii - ~SeqTrigger.isdecimal - ~SeqTrigger.isdigit - ~SeqTrigger.isidentifier - ~SeqTrigger.islower - ~SeqTrigger.isnumeric - ~SeqTrigger.isprintable - ~SeqTrigger.isspace - ~SeqTrigger.istitle - ~SeqTrigger.isupper - ~SeqTrigger.join - ~SeqTrigger.ljust - ~SeqTrigger.lower - ~SeqTrigger.lstrip - ~SeqTrigger.maketrans - ~SeqTrigger.partition - ~SeqTrigger.removeprefix - ~SeqTrigger.removesuffix - ~SeqTrigger.replace - ~SeqTrigger.rfind - ~SeqTrigger.rindex - ~SeqTrigger.rjust - ~SeqTrigger.rpartition - ~SeqTrigger.rsplit - ~SeqTrigger.rstrip - ~SeqTrigger.split - ~SeqTrigger.splitlines - ~SeqTrigger.startswith - ~SeqTrigger.strip - ~SeqTrigger.swapcase - ~SeqTrigger.title - ~SeqTrigger.translate - ~SeqTrigger.upper - ~SeqTrigger.zfill - - - - - - .. rubric:: Attributes - - .. autosummary:: - - ~SeqTrigger.IMMEDIATE - ~SeqTrigger.BITA_0 - ~SeqTrigger.BITA_1 - ~SeqTrigger.BITB_0 - ~SeqTrigger.BITB_1 - ~SeqTrigger.BITC_0 - ~SeqTrigger.BITC_1 - ~SeqTrigger.POSA_GT - ~SeqTrigger.POSA_LT - ~SeqTrigger.POSB_GT - ~SeqTrigger.POSB_LT - ~SeqTrigger.POSC_GT - ~SeqTrigger.POSC_LT - - \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.panda.phase_sorter.rst b/docs/user/generated/ophyd_async.panda.phase_sorter.rst deleted file mode 100644 index 76679476aa..0000000000 --- a/docs/user/generated/ophyd_async.panda.phase_sorter.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.panda.phase\_sorter -================================ - -.. currentmodule:: ophyd_async.panda - -.. autofunction:: phase_sorter \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.panda.pvi.rst b/docs/user/generated/ophyd_async.panda.pvi.rst deleted file mode 100644 index 22c0292459..0000000000 --- a/docs/user/generated/ophyd_async.panda.pvi.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.panda.pvi -====================== - -.. currentmodule:: ophyd_async.panda - -.. autofunction:: pvi \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.panda.rst b/docs/user/generated/ophyd_async.panda.rst deleted file mode 100644 index b7b9ab1230..0000000000 --- a/docs/user/generated/ophyd_async.panda.rst +++ /dev/null @@ -1,55 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -ophyd\_async.panda -================== - -.. automodule:: ophyd_async.panda - - - - - - - - .. rubric:: Functions - - .. autosummary:: - :toctree: - :nosignatures: - - seq_table_from_arrays - seq_table_from_rows - phase_sorter - - - - - - .. rubric:: Classes - - .. autosummary:: - :toctree: - :template: custom-class-template.rst - :nosignatures: - - PandA - PcapBlock - PulseBlock - PVIEntry - SeqBlock - SeqTable - SeqTableRow - SeqTrigger - PandaPcapController - - - - - - - - - diff --git a/docs/user/generated/ophyd_async.panda.seq_table_from_arrays.rst b/docs/user/generated/ophyd_async.panda.seq_table_from_arrays.rst deleted file mode 100644 index 0a0a86484a..0000000000 --- a/docs/user/generated/ophyd_async.panda.seq_table_from_arrays.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.panda.seq\_table\_from\_arrays -=========================================== - -.. currentmodule:: ophyd_async.panda - -.. autofunction:: seq_table_from_arrays \ No newline at end of file diff --git a/docs/user/generated/ophyd_async.panda.seq_table_from_rows.rst b/docs/user/generated/ophyd_async.panda.seq_table_from_rows.rst deleted file mode 100644 index bf864bd4b7..0000000000 --- a/docs/user/generated/ophyd_async.panda.seq_table_from_rows.rst +++ /dev/null @@ -1,6 +0,0 @@ -ophyd\_async.panda.seq\_table\_from\_rows -========================================= - -.. currentmodule:: ophyd_async.panda - -.. autofunction:: seq_table_from_rows \ No newline at end of file From 2d9ec4087b7429e8dd9408aecdccefd054a26328 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 12 Jul 2024 14:13:01 +0200 Subject: [PATCH 067/141] Updated pytango version to solve deprecation warning in calls to test_context --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cf59c4750f..b725b07fc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ dev = [ "pydata-sphinx-theme>=0.12", "pyepics>=3.4.2", "pyside6==6.7.0", - "pytango", + "pytango>=10.0.0rc1", "pytest", "pytest-asyncio", "pytest-cov", From b26c2396767e5c0b2ec6311f9a1152d0fc4e3d26 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 12 Jul 2024 14:52:24 +0200 Subject: [PATCH 068/141] Tango tests can only be run as forked pytests, meaning they are each run in a separate process. This is caused by backend Tango code. This unfortunately slows everything down. I added a method pytest_collection_modifyitems to conftest.py which marks all tests in the tests/tango directory as forked. --- pyproject.toml | 1 + tests/conftest.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d6d6b02380..77aac9afb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ dev = [ "pytest-asyncio", "pytest-cov", "pytest-faulthandler", + "pytest-forked", "pytest-rerunfailures", "pytest-timeout", "ruff", diff --git a/tests/conftest.py b/tests/conftest.py index 395a2d212a..87ac0d47c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -126,3 +126,11 @@ async def inner_coroutine(): @pytest.fixture def static_directory_provider(tmp_path: Path): return StaticDirectoryProvider(directory_path=tmp_path) + +def pytest_collection_modifyitems(config, items): + tango_dir = 'tests/tango' + + for item in items: + if tango_dir in str(item.fspath): + item.add_marker(pytest.mark.forked) + # Removed the else block to ensure tests outside tango_dir are not automatically skipped From 2744bfe0c1ebbf81b91b934dc35f73da90b9c0c4 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 12 Jul 2024 15:43:03 +0200 Subject: [PATCH 069/141] Fixed line length. --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 87ac0d47c9..ee83384aaa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -133,4 +133,5 @@ def pytest_collection_modifyitems(config, items): for item in items: if tango_dir in str(item.fspath): item.add_marker(pytest.mark.forked) - # Removed the else block to ensure tests outside tango_dir are not automatically skipped + # Removed the else block to ensure tests outside tango_dir are + # not automatically skipped From 504639b955833bdbdd1e573c9a0c4271e87235c2 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 12 Jul 2024 15:46:26 +0200 Subject: [PATCH 070/141] Fixed linting again --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index ee83384aaa..0b005f3525 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -129,7 +129,7 @@ def static_directory_provider(tmp_path: Path): def pytest_collection_modifyitems(config, items): tango_dir = 'tests/tango' - + for item in items: if tango_dir in str(item.fspath): item.add_marker(pytest.mark.forked) From e25524492068b3741612369b65f218acb3dd3175 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 12 Jul 2024 15:50:47 +0200 Subject: [PATCH 071/141] More linting fixes. --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0b005f3525..ee3721cb98 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -127,8 +127,9 @@ async def inner_coroutine(): def static_directory_provider(tmp_path: Path): return StaticDirectoryProvider(directory_path=tmp_path) + def pytest_collection_modifyitems(config, items): - tango_dir = 'tests/tango' + tango_dir = "tests/tango" for item in items: if tango_dir in str(item.fspath): From 00244e2d75a2c8ede5d8b6049576a6dda419a45d Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 22 Jul 2024 16:02:18 +0200 Subject: [PATCH 072/141] dgg2 should config and read the sample time --- src/ophyd_async/tango/device_controllers/dgg2.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/ophyd_async/tango/device_controllers/dgg2.py b/src/ophyd_async/tango/device_controllers/dgg2.py index 1615a29e80..c79e8760d0 100644 --- a/src/ophyd_async/tango/device_controllers/dgg2.py +++ b/src/ophyd_async/tango/device_controllers/dgg2.py @@ -9,7 +9,6 @@ from ophyd_async.core import ( AsyncStatus, ConfigSignal, - HintedSignal, ) from ophyd_async.core.utils import DEFAULT_TIMEOUT from ophyd_async.tango import ( @@ -40,18 +39,18 @@ def __init__(self, trl: str, name="", sources: dict = None) -> None: if not self.src_dict[key].startswith("/"): self.src_dict[key] = "/" + self.src_dict[key] - # Add sampletime as an unchached hinted signal + # Add sampletime is both a config signal and a readable with self.add_children_as_readables(ConfigSignal): self.sampletime = tango_signal_rw( float, self.trl + self.src_dict["sampletime"], device_proxy=self.proxy ) + self.add_readables([self.sampletime]) - with self.add_children_as_readables(HintedSignal): - self.remainingtime = tango_signal_rw( - float, - self.trl + self.src_dict["remainingtime"], - device_proxy=self.proxy, - ) + self.remainingtime = tango_signal_rw( + float, + self.trl + self.src_dict["remainingtime"], + device_proxy=self.proxy, + ) self.startandwaitfortimer = tango_signal_x( self.trl + self.src_dict["startandwaitfortimer"], device_proxy=self.proxy From 02a88133b7b9a0c028e839eb40045aed68830b8c Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 5 Aug 2024 09:24:02 +0200 Subject: [PATCH 073/141] Removed example devices which were only valid for Tango servers at DESY. --- .../tango/device_controllers/__init__.py | 5 - .../tango/device_controllers/dgg2.py | 95 ------------ .../tango/device_controllers/omsvme58.py | 138 ------------------ .../tango/device_controllers/sis3820.py | 73 --------- 4 files changed, 311 deletions(-) delete mode 100644 src/ophyd_async/tango/device_controllers/__init__.py delete mode 100644 src/ophyd_async/tango/device_controllers/dgg2.py delete mode 100644 src/ophyd_async/tango/device_controllers/omsvme58.py delete mode 100644 src/ophyd_async/tango/device_controllers/sis3820.py diff --git a/src/ophyd_async/tango/device_controllers/__init__.py b/src/ophyd_async/tango/device_controllers/__init__.py deleted file mode 100644 index 59cf2d18f6..0000000000 --- a/src/ophyd_async/tango/device_controllers/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from ophyd_async.tango.device_controllers.dgg2 import DGG2Timer -from ophyd_async.tango.device_controllers.omsvme58 import OmsVME58Motor -from ophyd_async.tango.device_controllers.sis3820 import SIS3820Counter - -__all__ = ("OmsVME58Motor", "SIS3820Counter", "DGG2Timer") diff --git a/src/ophyd_async/tango/device_controllers/dgg2.py b/src/ophyd_async/tango/device_controllers/dgg2.py deleted file mode 100644 index c79e8760d0..0000000000 --- a/src/ophyd_async/tango/device_controllers/dgg2.py +++ /dev/null @@ -1,95 +0,0 @@ -from __future__ import annotations - -import asyncio -from asyncio import Event -from typing import Optional - -from bluesky.protocols import Preparable, Triggerable - -from ophyd_async.core import ( - AsyncStatus, - ConfigSignal, -) -from ophyd_async.core.utils import DEFAULT_TIMEOUT -from ophyd_async.tango import ( - TangoReadableDevice, - tango_signal_r, - tango_signal_rw, - tango_signal_x, -) -from tango import DevState - - -# -------------------------------------------------------------------- -class DGG2Timer(TangoReadableDevice, Triggerable, Preparable): - # -------------------------------------------------------------------- - def __init__(self, trl: str, name="", sources: dict = None) -> None: - if sources is None: - sources = {} - self.trl = trl - self.src_dict["sampletime"] = sources.get("sampletime", "/SampleTime") - self.src_dict["remainingtime"] = sources.get("remainingtime", "/RemainingTime") - self.src_dict["startandwaitfortimer"] = sources.get( - "startandwaitfortimer", "/StartAndWaitForTimer" - ) - self.src_dict["start"] = sources.get("start", "/Start") - self.src_dict["state"] = sources.get("state", "/State") - - for key in self.src_dict: - if not self.src_dict[key].startswith("/"): - self.src_dict[key] = "/" + self.src_dict[key] - - # Add sampletime is both a config signal and a readable - with self.add_children_as_readables(ConfigSignal): - self.sampletime = tango_signal_rw( - float, self.trl + self.src_dict["sampletime"], device_proxy=self.proxy - ) - self.add_readables([self.sampletime]) - - self.remainingtime = tango_signal_rw( - float, - self.trl + self.src_dict["remainingtime"], - device_proxy=self.proxy, - ) - - self.startandwaitfortimer = tango_signal_x( - self.trl + self.src_dict["startandwaitfortimer"], device_proxy=self.proxy - ) - - self.start = tango_signal_x( - self.trl + self.src_dict["start"], device_proxy=self.proxy - ) - - self._state = tango_signal_r( - DevState, self.trl + self.src_dict["state"], self.proxy - ) - - TangoReadableDevice.__init__(self, trl, name) - self._set_success = True - - def prepare(self, p_time: float) -> AsyncStatus: - return self.sampletime.set(p_time) - - def trigger(self): - return AsyncStatus(self._trigger()) - - async def _trigger(self): - sample_time = await self.sampletime.get_value() - timeout = sample_time + DEFAULT_TIMEOUT - await self.start.trigger(wait=True, timeout=timeout) - await self._wait() - - async def _wait(self, event: Optional[Event] = None) -> None: - # await asyncio.sleep(0.5) - state = await self._state.get_value() - try: - while state == DevState.MOVING: - await asyncio.sleep(0.1) - state = await self._state.get_value() - except Exception as e: - raise RuntimeError(f"Error waiting for motor to stop: {e}") - finally: - if event: - event.set() - if state != DevState.ON: - raise RuntimeError(f"Motor did not stop correctly. State {state}") diff --git a/src/ophyd_async/tango/device_controllers/omsvme58.py b/src/ophyd_async/tango/device_controllers/omsvme58.py deleted file mode 100644 index 563141bd49..0000000000 --- a/src/ophyd_async/tango/device_controllers/omsvme58.py +++ /dev/null @@ -1,138 +0,0 @@ -from __future__ import annotations - -import asyncio -from asyncio import Event -from typing import Optional - -from bluesky.protocols import Movable, Stoppable - -from ophyd_async.core import ( - AsyncStatus, - ConfigSignal, - HintedSignal, - WatchableAsyncStatus, -) -from ophyd_async.core.signal import observe_value -from ophyd_async.core.utils import ( - DEFAULT_TIMEOUT, - CalculatableTimeout, - CalculateTimeout, - WatcherUpdate, -) -from ophyd_async.tango import ( - TangoReadableDevice, - tango_signal_r, - tango_signal_rw, - tango_signal_x, -) -from tango import DevState - - -# -------------------------------------------------------------------- -class OmsVME58Motor(TangoReadableDevice, Movable, Stoppable): - # -------------------------------------------------------------------- - def __init__(self, trl: str, name: str = "", sources: dict = None) -> None: - if sources is None: - sources = {} - self.src_dict["position"] = sources.get("position", "/Position") - self.src_dict["baserate"] = sources.get("baserate", "/BaseRate") - self.src_dict["slewrate"] = sources.get("slewrate", "/SlewRate") - self.src_dict["conversion"] = sources.get("conversion", "/Conversion") - self.src_dict["acceleration"] = sources.get("acceleration", "/Acceleration") - self.src_dict["stop"] = sources.get("stop", "/StopMove") - self.src_dict["state"] = sources.get("state", "/State") - - for key in self.src_dict: - if not self.src_dict[key].startswith("/"): - self.src_dict[key] = "/" + self.src_dict[key] - - with self.add_children_as_readables(HintedSignal): - self.position = tango_signal_rw( - float, trl + self.src_dict["position"], device_proxy=self.proxy - ) - with self.add_children_as_readables(ConfigSignal): - self.baserate = tango_signal_rw( - int, trl + self.src_dict["baserate"], device_proxy=self.proxy - ) - self.slewrate = tango_signal_rw( - int, trl + self.src_dict["slewrate"], device_proxy=self.proxy - ) - self.conversion = tango_signal_rw( - float, trl + self.src_dict["conversion"], device_proxy=self.proxy - ) - self.acceleration = tango_signal_rw( - int, trl + self.src_dict["acceleration"], device_proxy=self.proxy - ) - - self._stop = tango_signal_x(trl + self.src_dict["stop"], self.proxy) - self._state = tango_signal_r(DevState, trl + self.src_dict["state"], self.proxy) - - TangoReadableDevice.__init__(self, trl, name) - self._set_success = True - - @WatchableAsyncStatus.wrap - async def set( - self, - new_position: float, - timeout: CalculatableTimeout = CalculateTimeout, - ): - self._set_success = True - ( - old_position, - conversion, - velocity, - acceleration, - ) = await asyncio.gather( - self.position.get_value(), - self.conversion.get_value(), - self.slewrate.get_value(), - self.acceleration.get_value(), - ) - if timeout is CalculateTimeout: - assert velocity > 0, "Motor has zero velocity" - timeout = ( - (abs(new_position - old_position) * conversion / velocity) - + (2 * velocity / acceleration) - + DEFAULT_TIMEOUT - ) - - await self.position.set(new_position, wait=True, timeout=timeout) - - move_status = AsyncStatus(self._wait()) - - try: - async for current_position in observe_value( - self.position, done_status=move_status - ): - yield WatcherUpdate( - current=current_position, - initial=old_position, - target=new_position, - name=self.name, - ) - except RuntimeError as exc: - print(f"RuntimeError: {exc}") - raise - if not self._set_success: - raise RuntimeError("Motor was stopped") - - # -------------------------------------------------------------------- - def stop(self, success: bool = False) -> AsyncStatus: - self._set_success = success - return self._stop.trigger() - - # -------------------------------------------------------------------- - async def _wait(self, event: Optional[Event] = None) -> None: - await asyncio.sleep(0.5) - state = await self._state.get_value() - try: - while state == DevState.MOVING: - await asyncio.sleep(0.1) - state = await self._state.get_value() - except Exception as e: - raise RuntimeError(f"Error waiting for motor to stop: {e}") - finally: - if event: - event.set() - if state != DevState.ON: - raise RuntimeError(f"Motor did not stop correctly. State {state}") diff --git a/src/ophyd_async/tango/device_controllers/sis3820.py b/src/ophyd_async/tango/device_controllers/sis3820.py deleted file mode 100644 index 69083c9c84..0000000000 --- a/src/ophyd_async/tango/device_controllers/sis3820.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import annotations - -from typing import Dict - -from bluesky.protocols import Reading, Triggerable - -from ophyd_async.core import ( - DEFAULT_TIMEOUT, - AsyncStatus, - ConfigSignal, - HintedSignal, - StandardReadable, -) -from ophyd_async.tango import tango_signal_rw, tango_signal_x -from tango.asyncio import DeviceProxy - - -# -------------------------------------------------------------------- -class SIS3820Counter(StandardReadable, Triggerable): - # -------------------------------------------------------------------- - def __init__(self, trl: str, name: str = "", sources: dict = None) -> None: - if sources is None: - sources = {} - self.proxy = None - self.trl = trl - self.src_dict = { - "counts": sources.get("counts", "/Counts"), - "offset": sources.get("offset", "/Offset"), - "reset": sources.get("reset", "/Reset"), - } - - for key in self.src_dict: - if not self.src_dict[key].startswith("/"): - self.src_dict[key] = "/" + self.src_dict[key] - - with self.add_children_as_readables(HintedSignal): - self.counts = tango_signal_rw( - float, trl + self.src_dict["counts"], device_proxy=self.proxy - ) - with self.add_children_as_readables(ConfigSignal): - self.offset = tango_signal_rw( - float, trl + self.src_dict["offset"], device_proxy=self.proxy - ) - - self.reset = tango_signal_x( - trl + self.src_dict["reset"], device_proxy=self.proxy - ) - - StandardReadable.__init__(self, name=name) - self._set_success = True - - async def connect( - self, - mock: bool = False, - timeout: float = DEFAULT_TIMEOUT, - force_reconnect: bool = False, - ): - async def closure(): - self.proxy = await DeviceProxy(self.trl) - return self - - await closure() - await super().connect(mock=mock, timeout=timeout) - - async def read(self) -> Dict[str, Reading]: - ret = await super().read() - return ret - - def trigger(self) -> AsyncStatus: - return AsyncStatus(self._trigger()) - - async def _trigger(self) -> None: - await self.reset.trigger() From 9db25b24818e68c46b4e183f72f229394eccafb8 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 5 Aug 2024 09:25:47 +0200 Subject: [PATCH 074/141] Removed unused, commented code --- src/ophyd_async/tango/base_devices/base_device.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index 590191a005..f0c8189c3a 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -52,17 +52,3 @@ async def closure(): self.register_signals() await super().connect(mock=mock, timeout=timeout) - - # -------------------------------------------------------------------- - # @AsyncStatus.wrap - # async def stage(self) -> None: - # for sig in self._readables + self._configurables: - # if hasattr(sig, "is_cachable") and sig.is_cachable(): - # await sig.stage().task - # - # # -------------------------------------------------------------------- - # @AsyncStatus.wrap - # async def unstage(self) -> None: - # for sig in self._readables + self._configurables: - # if hasattr(sig, "is_cachable") and sig.is_cachable(): - # await sig.unstage().task From f858347d5cd1c70a2b5be109af64ec61f4e9ec3c Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 6 Aug 2024 09:10:53 +0200 Subject: [PATCH 075/141] Removed unnecessary print statements --- src/ophyd_async/tango/_backend/_tango_transport.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index d8c2c66938..dda3a080a5 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -401,10 +401,6 @@ def get_dtype_extended(datatype): # DevState tango type does not have numpy equivalents dtype = get_dtype(datatype) if dtype == np.object_: - print( - f"{datatype.__args__[1].__args__[0]=}," - f"{datatype.__args__[1].__args__[0]==Enum}" - ) if datatype.__args__[1].__args__[0] == DevState: dtype = CmdArgType.DevState return dtype From 8a8d4dfeb65e5c082fae9e137c1b3cdaa07e3a15 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 12 Aug 2024 13:57:35 +0200 Subject: [PATCH 076/141] New tests for tango_transport and some modification to test devices --- tests/tango/test_base_device.py | 16 +- tests/tango/test_tango_transport.py | 486 ++++++++++++++++++++++++++++ 2 files changed, 499 insertions(+), 3 deletions(-) create mode 100644 tests/tango/test_tango_transport.py diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 796e392ef1..29fcce7ca8 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -20,7 +20,7 @@ ) from tango.asyncio import DeviceProxy from tango.asyncio_executor import set_global_executor -from tango.server import Device, attribute +from tango.server import Device, attribute, command from tango.test_context import MultiDeviceTestContext from tango.test_utils import assert_close @@ -43,11 +43,16 @@ class TestDevice(Device): _array = [[1, 2, 3], [4, 5, 6]] + _justvalue = 5 + _limitedvalue = 3 - @attribute(dtype=int) + @attribute(dtype=int, access=AttrWriteType.READ_WRITE, polling_period=100) def justvalue(self): - return 5 + return self._justvalue + + def write_justvalue(self, value: int): + self._justvalue = value @attribute( dtype=float, @@ -78,6 +83,11 @@ def limitedvalue(self) -> float: def write_limitedvalue(self, value: float): self._limitedvalue = value + @command + def clear(self) -> str: + # self.info_stream("Received clear command") + return "Received clear command" + # -------------------------------------------------------------------- class TestReadableDevice(TangoReadableDevice): diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py new file mode 100644 index 0000000000..7be491cee7 --- /dev/null +++ b/tests/tango/test_tango_transport.py @@ -0,0 +1,486 @@ +import asyncio +from enum import Enum + +import numpy as np +import numpy.typing as npt +import pytest +from test_tango_signals import ( + make_backend, + prepare_device, +) + +from ophyd_async.tango._backend._tango_transport import ( + AttributeProxy, + CommandProxy, + ensure_proper_executor, + get_dtype_extended, + get_python_type, + get_tango_trl, + get_trl_descriptor, +) +from tango import ( + CmdArgType, + DevState, +) +from tango.asyncio import DeviceProxy +from tango.asyncio_executor import ( + AsyncioExecutor, + get_global_executor, +) + + +# -------------------------------------------------------------------- +@pytest.fixture(scope="module") +async def device_proxy(tango_test_device): + return await DeviceProxy(tango_test_device) + + +# -------------------------------------------------------------------- +@pytest.fixture(scope="module") +async def transport(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + return await make_backend(float, source, connect=True) + + +# -------------------------------------------------------------------- +class HelperClass: + @ensure_proper_executor + async def mock_func(self): + return "executed" + + +# Test function +@pytest.mark.asyncio +async def test_ensure_proper_executor(): + # Instantiate the helper class and call the decorated method + helper_instance = HelperClass() + result = await helper_instance.mock_func() + + # Assertions + assert result == "executed" + assert isinstance(get_global_executor(), AsyncioExecutor) + + +# -------------------------------------------------------------------- +@pytest.mark.parametrize( + "tango_type, expected", + [ + (CmdArgType.DevVoid, (False, None, "string")), + (CmdArgType.DevBoolean, (False, bool, "integer")), + (CmdArgType.DevShort, (False, int, "integer")), + (CmdArgType.DevLong, (False, int, "integer")), + (CmdArgType.DevFloat, (False, float, "number")), + (CmdArgType.DevDouble, (False, float, "number")), + (CmdArgType.DevUShort, (False, int, "integer")), + (CmdArgType.DevULong, (False, int, "integer")), + (CmdArgType.DevString, (False, str, "string")), + (CmdArgType.DevVarCharArray, (True, list[str], "string")), + (CmdArgType.DevVarShortArray, (True, int, "integer")), + (CmdArgType.DevVarLongArray, (True, int, "integer")), + (CmdArgType.DevVarFloatArray, (True, float, "number")), + (CmdArgType.DevVarDoubleArray, (True, float, "number")), + (CmdArgType.DevVarUShortArray, (True, int, "integer")), + (CmdArgType.DevVarULongArray, (True, int, "integer")), + (CmdArgType.DevVarStringArray, (True, str, "string")), + # (CmdArgType.DevVarLongStringArray, (True, str, "string")), + # (CmdArgType.DevVarDoubleStringArray, (True, str, "string")), + (CmdArgType.DevState, (False, CmdArgType.DevState, "string")), + (CmdArgType.ConstDevString, (False, str, "string")), + (CmdArgType.DevVarBooleanArray, (True, bool, "integer")), + (CmdArgType.DevUChar, (False, int, "integer")), + (CmdArgType.DevLong64, (False, int, "integer")), + (CmdArgType.DevULong64, (False, int, "integer")), + (CmdArgType.DevVarLong64Array, (True, int, "integer")), + (CmdArgType.DevVarULong64Array, (True, int, "integer")), + (CmdArgType.DevEncoded, (False, list[str], "string")), + (CmdArgType.DevEnum, (False, Enum, "string")), + # (CmdArgType.DevPipeBlob, (False, list[str], "string")), + ], +) +def test_get_python_type(tango_type, expected): + assert get_python_type(tango_type) == expected + + +# -------------------------------------------------------------------- +@pytest.mark.parametrize( + "datatype, expected", + [ + (npt.NDArray[np.float64], np.dtype("float64")), + (npt.NDArray[np.int8], np.dtype("int8")), + (npt.NDArray[np.uint8], np.dtype("uint8")), + (npt.NDArray[np.int32], np.dtype("int32")), + (npt.NDArray[np.int64], np.dtype("int64")), + (npt.NDArray[np.uint16], np.dtype("uint16")), + (npt.NDArray[np.uint32], np.dtype("uint32")), + (npt.NDArray[np.uint64], np.dtype("uint64")), + (npt.NDArray[np.bool_], np.dtype("bool")), + (npt.NDArray[DevState], CmdArgType.DevState), + (npt.NDArray[np.str_], np.dtype("str")), + (npt.NDArray[np.float32], np.dtype("float32")), + (npt.NDArray[np.complex64], np.dtype("complex64")), + (npt.NDArray[np.complex128], np.dtype("complex128")), + ], +) +def test_get_dtype_extended(datatype, expected): + assert get_dtype_extended(datatype) == expected + + +# -------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "datatype, tango_resource, expected_descriptor", + [ + ( + int, + "test/device/1/justvalue", + {"source": "test/device/1/justvalue", "dtype": "integer", "shape": []}, + ), + ( + float, + "test/device/1/limitedvalue", + {"source": "test/device/1/limitedvalue", "dtype": "number", "shape": []}, + ), + ( + npt.NDArray[float], + "test/device/1/array", + {"source": "test/device/1/array", "dtype": "array", "shape": [2, 3]}, + ), + # Add more test cases as needed + ], +) +async def test_get_trl_descriptor( + tango_test_device, datatype, tango_resource, expected_descriptor +): + proxy = await DeviceProxy(tango_test_device) + tr_configs = { + tango_resource.split("/")[-1]: await proxy.get_attribute_config( + tango_resource.split("/")[-1] + ) + } + descriptor = get_trl_descriptor(datatype, tango_resource, tr_configs) + assert descriptor == expected_descriptor + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize( + "trl, proxy_needed, expected_type, should_raise", + [ + ("test/device/1/justvalue", True, AttributeProxy, False), + ("test/device/1/justvalue", False, AttributeProxy, False), + ("test/device/1/clear", True, CommandProxy, False), + ("test/device/1/clear", False, CommandProxy, False), + ("test/device/1/nonexistent", True, None, True), + ], +) +async def test_get_tango_trl( + tango_test_device, trl, proxy_needed, expected_type, should_raise +): + proxy = await DeviceProxy(tango_test_device) if proxy_needed else None + if should_raise: + with pytest.raises(RuntimeError): + await get_tango_trl(trl, proxy) + else: + result = await get_tango_trl(trl, proxy) + assert isinstance(result, expected_type) + + +# -------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.parametrize("attr", ["justvalue", "array"]) +async def test_attribute_proxy_get(device_proxy, attr): + attr_proxy = AttributeProxy(device_proxy, attr) + val = None + val = await attr_proxy.get() + assert val is not None + + +@pytest.mark.asyncio +@pytest.mark.parametrize("attr", ["justvalue", "array"]) +async def test_attribute_proxy_put(device_proxy, attr): + attr_proxy = AttributeProxy(device_proxy, attr) + old_value = await attr_proxy.get() + new_value = old_value + 1 + await attr_proxy.put(new_value, wait=True) + updated_value = await attr_proxy.get() + if isinstance(new_value, np.ndarray): + assert np.all(updated_value == new_value) + else: + assert updated_value == new_value + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize( + "attr, new_value", [("justvalue", 10), ("array", np.array([[2, 3, 4], [5, 6, 7]]))] +) +async def test_attribute_proxy_get_w_value(device_proxy, attr, new_value): + attr_proxy = AttributeProxy(device_proxy, attr) + await attr_proxy.put(new_value) + attr_proxy_value = await attr_proxy.get() + if isinstance(new_value, np.ndarray): + assert np.all(attr_proxy_value == new_value) + else: + assert attr_proxy_value == new_value + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_attribute_get_config(device_proxy): + attr_proxy = AttributeProxy(device_proxy, "justvalue") + config = await attr_proxy.get_config() + assert config.writable is not None + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_attribute_get_reading(device_proxy): + attr_proxy = AttributeProxy(device_proxy, "justvalue") + reading = await attr_proxy.get_reading() + assert reading["value"] is not None + + +# -------------------------------------------------------------------- +def test_attribute_has_subscription(device_proxy): + attr_proxy = AttributeProxy(device_proxy, "justvalue") + expected = bool(attr_proxy._callback) + has_subscription = attr_proxy.has_subscription() + assert has_subscription is expected + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_attribute_subscribe_callback(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + backend = await make_backend(float, source) + attr_proxy = backend.proxies[source] + val = None + + def callback(reading, value): + print("Callback called") + nonlocal val + val = value + + attr_proxy.subscribe_callback(callback) + assert attr_proxy.has_subscription() + old_value = await attr_proxy.get() + new_value = old_value + 1 + await attr_proxy.put(new_value) + await asyncio.sleep(0.2) + attr_proxy.unsubscribe_callback() + assert val == new_value + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_attribute_unsubscribe_callback(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + backend = await make_backend(float, source) + attr_proxy = backend.proxies[source] + + def callback(reading, value): + pass + + attr_proxy.subscribe_callback(callback) + assert attr_proxy.has_subscription() + attr_proxy.unsubscribe_callback() + assert not attr_proxy.has_subscription() + + +# -------------------------------------------------------------------- +def test_attribute_set_polling(device_proxy): + attr_proxy = AttributeProxy(device_proxy, "justvalue") + attr_proxy.set_polling(True, 0.1, 1, 0.1) + assert attr_proxy._allow_polling + assert attr_proxy._polling_period == 0.1 + assert attr_proxy._abs_change == 1 + assert attr_proxy._rel_change == 0.1 + attr_proxy.set_polling(False) + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_attribute_poll(device_proxy): + attr_proxy = AttributeProxy(device_proxy, "justvalue") + attr_proxy.set_polling(True, 0.1, 1) + val = None + + def callback(reading, value): + nonlocal val + val = value + + attr_proxy.subscribe_callback(callback) + current_value = await attr_proxy.get() + new_value = current_value + 2 + await attr_proxy.put(new_value) + polling_period = attr_proxy._polling_period + await asyncio.sleep(polling_period) + attr_proxy.set_polling(False) + assert val is not None + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_command_put(device_proxy): + cmd_proxy = CommandProxy(device_proxy, "clear") + await cmd_proxy.put(None, wait=True, timeout=1.0) + assert cmd_proxy._last_reading["value"] is not None + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_command_get(device_proxy): + cmd_proxy = CommandProxy(device_proxy, "clear") + await cmd_proxy.put(None, wait=True, timeout=1.0) + reading = cmd_proxy._last_reading + assert reading["value"] is not None + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_command_get_config(device_proxy): + cmd_proxy = CommandProxy(device_proxy, "clear") + config = await cmd_proxy.get_config() + assert config.out_type is not None + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_command_get_reading(device_proxy): + cmd_proxy = CommandProxy(device_proxy, "clear") + await cmd_proxy.put(None, wait=True, timeout=1.0) + reading = await cmd_proxy.get_reading() + assert reading["value"] is not None + + +# -------------------------------------------------------------------- +def test_command_set_polling(device_proxy): + cmd_proxy = CommandProxy(device_proxy, "clear") + cmd_proxy.set_polling(True, 0.1) + # Set polling in the command proxy currently does nothing + assert True + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_init(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + transport = await make_backend(float, source, connect=False) + assert transport is not None + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_source(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + transport = await make_backend(float, source) + transport_source = transport.source("") + assert transport_source == source + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_connect(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + backend = await make_backend(float, source, connect=True) + assert backend is not None + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_connect_and_store_config(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + transport = await make_backend(float, source, connect=False) + await transport._connect_and_store_config(source) + assert transport.trl_configs[source] is not None + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_put(transport): + source = transport.source("") + await transport.put(2.0) + val = await transport.proxies[source].get_w_value() + assert val == 2.0 + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_get_datakey(transport): + source = transport.source("") + datakey = await transport.get_datakey(source) + assert datakey["source"] == source + assert datakey["dtype"] == "number" + assert datakey["shape"] == [] + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_get_reading(transport): + reading = await transport.get_reading() + assert reading["value"] == 1.0 + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_get_value(transport): + value = await transport.get_value() + assert value == 1.0 + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_get_setpoint(transport): + new_setpoint = 2.0 + await transport.put(new_setpoint) + setpoint = await transport.get_setpoint() + assert setpoint == new_setpoint + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_set_callback(transport): + val = None + + def callback(reading, value): + nonlocal val + val = value + + transport.set_callback(callback) + current_value = await transport.get_value() + new_value = current_value + 2 + await transport.put(new_value) + await asyncio.sleep(0.1) + transport.set_callback(None) + assert val == new_value + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_set_polling(transport): + source = transport.source("") + transport.set_polling(True, 0.1, 1, 0.1) + assert transport.polling == (True, 0.1, 1, 0.1) + assert transport.proxies[source]._allow_polling + assert transport.proxies[source]._polling_period == 0.1 + assert transport.proxies[source]._abs_change == 1 + assert transport.proxies[source]._rel_change == 0.1 + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize("allow", [True, False]) +async def test_tango_transport_allow_events(transport, allow): + transport.allow_events(allow) + assert transport.support_events == allow From f6d5642b1d4dd166a71c5ce9c58c3a0bc9747a4a Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 12 Aug 2024 14:29:43 +0200 Subject: [PATCH 077/141] Redefining test devices in test_tango_transport. Ruff didn't like importing these instead of the classes --- tests/tango/test_tango_transport.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index 7be491cee7..1da1da0d92 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -4,7 +4,9 @@ import numpy as np import numpy.typing as npt import pytest +from test_base_device import TestDevice from test_tango_signals import ( + EchoDevice, make_backend, prepare_device, ) @@ -26,7 +28,33 @@ from tango.asyncio_executor import ( AsyncioExecutor, get_global_executor, + set_global_executor, ) +from tango.test_context import MultiDeviceTestContext + + +# -------------------------------------------------------------------- +@pytest.fixture(scope="module") +def tango_test_device(): + with MultiDeviceTestContext( + [{"class": TestDevice, "devices": [{"name": "test/device/1"}]}], process=True + ) as context: + yield context.get_device_access("test/device/1") + + +# -------------------------------------------------------------------- +@pytest.fixture(scope="module") +def echo_device(): + with MultiDeviceTestContext( + [{"class": EchoDevice, "devices": [{"name": "test/device/1"}]}], process=True + ) as context: + yield context.get_device_access("test/device/1") + + +# -------------------------------------------------------------------- +@pytest.fixture(autouse=True) +def reset_tango_asyncio(): + set_global_executor(None) # -------------------------------------------------------------------- @@ -200,6 +228,7 @@ async def test_attribute_proxy_get(device_proxy, attr): assert val is not None +# -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize("attr", ["justvalue", "array"]) async def test_attribute_proxy_put(device_proxy, attr): From f3932f1132b486a147f2aa25fd8b65b36bc61b40 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 12 Aug 2024 14:51:01 +0200 Subject: [PATCH 078/141] Fixed import from core._utils --- src/ophyd_async/tango/signal/signal.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 3629dca080..3c3445cd54 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -4,8 +4,7 @@ from typing import Optional, Type, Union -from ophyd_async.core import SignalR, SignalRW, SignalW, SignalX, T -from ophyd_async.core.utils import DEFAULT_TIMEOUT +from ophyd_async.core import DEFAULT_TIMEOUT, SignalR, SignalRW, SignalW, SignalX, T from ophyd_async.tango._backend import TangoTransport from tango import AttrWriteType, CmdArgType from tango import DeviceProxy as SyncDeviceProxy From da15290108857c2a5ef28d563a834efdd08c634c Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 13 Aug 2024 13:09:28 +0200 Subject: [PATCH 079/141] Improved test coverage --- tests/tango/test_base_device.py | 13 ++ tests/tango/test_tango_signals.py | 243 +++++++++++++++++++++++++++++- 2 files changed, 255 insertions(+), 1 deletion(-) diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 29fcce7ca8..057c17de32 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -44,6 +44,8 @@ class TestDevice(Device): _array = [[1, 2, 3], [4, 5, 6]] _justvalue = 5 + _writeonly = 6 + _readonly = 7 _limitedvalue = 3 @@ -54,6 +56,17 @@ def justvalue(self): def write_justvalue(self, value: int): self._justvalue = value + @attribute(dtype=int, access=AttrWriteType.WRITE, polling_period=100) + def writeonly(self): + return self._writeonly + + def write_writeonly(self, value: int): + self._writeonly = value + + @attribute(dtype=int, access=AttrWriteType.READ, polling_period=100) + def readonly(self): + return self._readonly + @attribute( dtype=float, access=AttrWriteType.READ_WRITE, diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index 467ea1236f..2ebc487415 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -9,9 +9,17 @@ import numpy.typing as npt import pytest from bluesky.protocols import Reading +from test_base_device import TestDevice -from ophyd_async.core import SignalBackend, T +from ophyd_async.core import SignalBackend, SignalR, SignalRW, SignalW, SignalX, T from ophyd_async.tango._backend import TangoTransport +from ophyd_async.tango.signal import ( + tango_signal_auto, + tango_signal_r, + tango_signal_rw, + tango_signal_w, + tango_signal_x, +) from tango import AttrDataFormat, AttrWriteType, DevState from tango.asyncio import DeviceProxy from tango.asyncio_executor import set_global_executor @@ -115,6 +123,18 @@ class TestEnum(IntEnum): ) +# -------------------------------------------------------------------- +# TestDevice +# -------------------------------------------------------------------- +# -------------------------------------------------------------------- +@pytest.fixture(scope="module") +def tango_test_device(): + with MultiDeviceTestContext( + [{"class": TestDevice, "devices": [{"name": "test/device/1"}]}], process=True + ) as context: + yield context.get_device_access("test/device/1") + + # -------------------------------------------------------------------- # Echo device # -------------------------------------------------------------------- @@ -386,3 +406,224 @@ async def test_backend_get_put_monitor_cmd( tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] await asyncio.gather(*tasks) del echo_device + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize( + "pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy", + [ + (pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy) + for ( + pv, + tango_type, + d_format, + py_type, + initial_value, + put_value, + ) in ATTRIBUTES_SET + for use_proxy in [True, False] + ], + ids=[f"{x[0]}_{use_proxy}" for x in ATTRIBUTES_SET for use_proxy in [True, False]], +) +async def test_tango_signal_r( + echo_device: str, + pv: str, + tango_type: str, + d_format: AttrDataFormat, + py_type: Type[T], + initial_value: T, + put_value: T, + use_proxy: bool, +): + await prepare_device(echo_device, pv, initial_value) + source = echo_device + "/" + pv + proxy = await DeviceProxy(echo_device) if use_proxy else None + + timeout = 0.1 + signal = tango_signal_r( + datatype=py_type, + read_trl=source, + device_proxy=proxy, + timeout=timeout, + name="test_signal", + ) + await signal.connect() + reading = await signal.read() + assert_close(reading["test_signal"]["value"], initial_value) + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize( + "pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy", + [ + (pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy) + for ( + pv, + tango_type, + d_format, + py_type, + initial_value, + put_value, + ) in ATTRIBUTES_SET + for use_proxy in [True, False] + ], + ids=[f"{x[0]}_{use_proxy}" for x in ATTRIBUTES_SET for use_proxy in [True, False]], +) +async def test_tango_signal_w( + echo_device: str, + pv: str, + tango_type: str, + d_format: AttrDataFormat, + py_type: Type[T], + initial_value: T, + put_value: T, + use_proxy: bool, +): + await prepare_device(echo_device, pv, initial_value) + source = echo_device + "/" + pv + proxy = await DeviceProxy(echo_device) if use_proxy else None + + timeout = 0.1 + signal = tango_signal_w( + datatype=py_type, + write_trl=source, + device_proxy=proxy, + timeout=timeout, + name="test_signal", + ) + await signal.connect() + status = signal.set(put_value, wait=True, timeout=timeout) + await status + assert status.done is True and status.success is True + + status = signal.set(put_value, wait=False, timeout=timeout) + await status + assert status.done is True and status.success is True + + status = signal.set(put_value, wait=True) + await status + assert status.done is True and status.success is True + + status = signal.set(put_value, wait=False) + await status + assert status.done is True and status.success is True + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize( + "pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy", + [ + (pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy) + for ( + pv, + tango_type, + d_format, + py_type, + initial_value, + put_value, + ) in ATTRIBUTES_SET + for use_proxy in [True, False] + ], + ids=[f"{x[0]}_{use_proxy}" for x in ATTRIBUTES_SET for use_proxy in [True, False]], +) +async def test_tango_signal_rw( + echo_device: str, + pv: str, + tango_type: str, + d_format: AttrDataFormat, + py_type: Type[T], + initial_value: T, + put_value: T, + use_proxy: bool, +): + await prepare_device(echo_device, pv, initial_value) + source = echo_device + "/" + pv + proxy = await DeviceProxy(echo_device) if use_proxy else None + + timeout = 0.1 + signal = tango_signal_rw( + datatype=py_type, + read_trl=source, + write_trl=source, + device_proxy=proxy, + timeout=timeout, + name="test_signal", + ) + await signal.connect() + reading = await signal.read() + assert_close(reading["test_signal"]["value"], initial_value) + await signal.set(put_value) + location = await signal.locate() + assert_close(location["setpoint"], put_value) + assert_close(location["readback"], put_value) + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize("use_proxy", [True, False]) +async def test_tango_signal_x(tango_test_device: str, use_proxy: bool): + proxy = await DeviceProxy(tango_test_device) if use_proxy else None + timeout = 0.1 + signal = tango_signal_x( + write_trl=tango_test_device + "/" + "clear", + device_proxy=proxy, + timeout=timeout, + name="test_signal", + ) + await signal.connect() + status = signal.trigger() + await status + assert status.done is True and status.success is True + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize("use_proxy", [True, False]) +@pytest.mark.parametrize( + "attr_type", + [("justvalue", int), ("writeonly", int), ("readonly", int), ("clear", None)], +) +async def test_tango_signal_auto( + tango_test_device: str, attr_type: tuple[str, Type[T]], use_proxy: bool +): + proxy = await DeviceProxy(tango_test_device) if use_proxy else None + attr, py_type = attr_type + + timeout = 0.1 + signal = tango_signal_auto( + datatype=py_type, + full_trl=tango_test_device + "/" + attr, + device_proxy=proxy, + ) + await signal.connect() + + if isinstance(signal, SignalRW): + reading = await signal.read() + assert_close(reading[""]["value"], 5) + await signal.set(8, timeout=timeout) + location = await signal.locate() + assert_close(location["setpoint"], 8) + assert_close(location["readback"], 8) + return + + if isinstance(signal, SignalR): + reading = await signal.read() + assert reading[""]["value"] == 7 + return + + if isinstance(signal, SignalW): + status = signal.set(1, timeout=timeout) + await status + assert status.done is True and status.success is True + return + + if isinstance(signal, SignalX): + status = signal.trigger() + await status + assert status.done is True and status.success is True + return + + assert False From 17013b61efcffcca1867911809795406d536847a Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 13 Aug 2024 16:07:36 +0200 Subject: [PATCH 080/141] wip to improve coverage to TangoProxy.put() --- tests/tango/test_tango_transport.py | 103 ++++++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 7 deletions(-) diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index 1da1da0d92..715592256e 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -23,6 +23,7 @@ from tango import ( CmdArgType, DevState, + GreenMode, ) from tango.asyncio import DeviceProxy from tango.asyncio_executor import ( @@ -30,9 +31,22 @@ get_global_executor, set_global_executor, ) +from tango.server import Device, command from tango.test_context import MultiDeviceTestContext +# -------------------------------------------------------------------- +class DeviceAsynch(Device): + green_mode = GreenMode.Asyncio + + @command( + polling_period=100, + ) + async def slow_command(self) -> str: + await asyncio.sleep(0.2) + return "Completed slow command" + + # -------------------------------------------------------------------- @pytest.fixture(scope="module") def tango_test_device(): @@ -51,6 +65,15 @@ def echo_device(): yield context.get_device_access("test/device/1") +# -------------------------------------------------------------------- +@pytest.fixture(scope="module") +def tango_test_device_asynch(): + with MultiDeviceTestContext( + [{"class": DeviceAsynch, "devices": [{"name": "test/device/1"}]}], process=True + ) as context: + yield context.get_device_access("test/device/1") + + # -------------------------------------------------------------------- @pytest.fixture(autouse=True) def reset_tango_asyncio(): @@ -63,6 +86,12 @@ async def device_proxy(tango_test_device): return await DeviceProxy(tango_test_device) +# -------------------------------------------------------------------- +@pytest.fixture(scope="module") +async def device_proxy_asynch(tango_test_device_asynch): + return await DeviceProxy(tango_test_device_asynch) + + # -------------------------------------------------------------------- @pytest.fixture(scope="module") async def transport(echo_device): @@ -124,10 +153,17 @@ async def test_ensure_proper_executor(): (CmdArgType.DevEncoded, (False, list[str], "string")), (CmdArgType.DevEnum, (False, Enum, "string")), # (CmdArgType.DevPipeBlob, (False, list[str], "string")), + (float, (False, float, "number")), ], ) def test_get_python_type(tango_type, expected): - assert get_python_type(tango_type) == expected + if tango_type is not float: + assert get_python_type(tango_type) == expected + else: + # get_python_type should raise a TypeError + with pytest.raises(TypeError) as exc_info: + get_python_type(tango_type) + assert str(exc_info.value) == "Unknown TangoType" # -------------------------------------------------------------------- @@ -305,6 +341,12 @@ def callback(reading, value): attr_proxy.unsubscribe_callback() assert val == new_value + attr_proxy.set_polling(False) + attr_proxy.support_events = False + with pytest.raises(RuntimeError) as exc_info: + attr_proxy.subscribe_callback(callback) + assert "Cannot set a callback" in str(exc_info.value) + # -------------------------------------------------------------------- @pytest.mark.asyncio @@ -338,29 +380,76 @@ def test_attribute_set_polling(device_proxy): @pytest.mark.asyncio async def test_attribute_poll(device_proxy): attr_proxy = AttributeProxy(device_proxy, "justvalue") - attr_proxy.set_polling(True, 0.1, 1) - val = None def callback(reading, value): nonlocal val val = value + def bad_callback(): + pass + + # Test polling with absolute change + val = None + attr_proxy.set_polling(True, 0.1, 1, 1.0) attr_proxy.subscribe_callback(callback) current_value = await attr_proxy.get() new_value = current_value + 2 await attr_proxy.put(new_value) polling_period = attr_proxy._polling_period await asyncio.sleep(polling_period) - attr_proxy.set_polling(False) assert val is not None + attr_proxy.unsubscribe_callback() + + # Test polling with relative change + val = None + attr_proxy.set_polling(True, 0.1, 100, 0.1) + attr_proxy.subscribe_callback(callback) + current_value = await attr_proxy.get() + new_value = current_value * 2 + await attr_proxy.put(new_value) + polling_period = attr_proxy._polling_period + await asyncio.sleep(polling_period) + assert val is not None + attr_proxy.unsubscribe_callback() + + # Test polling with small changes. This should not update last_reading + attr_proxy.set_polling(True, 0.1, 100, 1.0) + attr_proxy.subscribe_callback(callback) + await asyncio.sleep(0.2) + current_value = await attr_proxy.get() + new_value = current_value + 1 + val = None + await attr_proxy.put(new_value) + polling_period = attr_proxy._polling_period + await asyncio.sleep(polling_period * 2) + assert val is None + attr_proxy.unsubscribe_callback() + + # Test polling with bad callback + attr_proxy.subscribe_callback(bad_callback) + await asyncio.sleep(0.2) + assert str(attr_proxy._poll_task.exception()) == "Could not poll the attribute" + attr_proxy.unsubscribe_callback() # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_command_put(device_proxy): +async def test_command_proxy_put_wait(device_proxy): cmd_proxy = CommandProxy(device_proxy, "clear") - await cmd_proxy.put(None, wait=True, timeout=1.0) - assert cmd_proxy._last_reading["value"] is not None + + cmd_proxy._last_reading = None + await cmd_proxy.put(None, wait=True) + assert cmd_proxy._last_reading["value"] == "Received clear command" + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_command_proxy_put_nowait(device_proxy_asynch): + cmd_proxy = CommandProxy(device_proxy_asynch, "slow_command") + + cmd_proxy._last_reading = None + await cmd_proxy.put(None, wait=False, timeout=0.5) + assert cmd_proxy._last_reading["value"] == "Completed slow command" # -------------------------------------------------------------------- From 20edee3700e61e2ccb633c9d84d4d5d2cab4094e Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 13 Aug 2024 16:10:02 +0200 Subject: [PATCH 081/141] write_attribute_asynch doesn't need to be awaited --- src/ophyd_async/tango/_backend/_tango_transport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index dda3a080a5..95d6482d2b 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -186,7 +186,7 @@ async def put( if wait: await self._proxy.write_attribute(self._name, value) else: - rid = await self._proxy.write_attribute_asynch(self._name, value) + rid = self._proxy.write_attribute_asynch(self._name, value) if timeout: finished = False while not finished: @@ -360,7 +360,7 @@ async def put( val = await self._proxy.command_inout(self._name, value) else: val = None - rid = await self._proxy.command_inout_asynch(self._name, value) + rid = self._proxy.command_inout_asynch(self._name, value) if timeout: finished = False while not finished: From 0d14e64559e99af5dbf97ec3776fa4da4e9c7897 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 13 Aug 2024 16:14:42 +0200 Subject: [PATCH 082/141] put will now timeout properly --- src/ophyd_async/tango/_backend/_tango_transport.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 95d6482d2b..14ef3ed743 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -188,6 +188,7 @@ async def put( else: rid = self._proxy.write_attribute_asynch(self._name, value) if timeout: + start_time = time.time() finished = False while not finished: try: @@ -195,6 +196,8 @@ async def put( finished = True except Exception: await asyncio.sleep(A_BIT) + if time.time() - start_time > timeout: + raise TimeoutError("Timeout while waiting for write reply") # -------------------------------------------------------------------- @ensure_proper_executor @@ -362,6 +365,7 @@ async def put( val = None rid = self._proxy.command_inout_asynch(self._name, value) if timeout: + start_time = time.time() finished = False while not finished: try: @@ -369,6 +373,10 @@ async def put( finished = True except Exception: await asyncio.sleep(A_BIT) + if time.time() - start_time > timeout: + raise TimeoutError( + "Timeout while waiting for command reply" + ) self._last_reading = { "value": val, From 14cb598ce3fca2b0d1a66aaee5364ed018c9d074 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 14 Aug 2024 13:44:29 +0200 Subject: [PATCH 083/141] New implementation of put without using timeout arg of _reply functions --- .../tango/_backend/_tango_transport.py | 113 ++++++++++++------ tests/tango/test_base_device.py | 15 +++ tests/tango/test_tango_transport.py | 79 +++++++----- 3 files changed, 143 insertions(+), 64 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 14ef3ed743..65e597dccb 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -11,6 +11,7 @@ from ophyd_async.core import ( DEFAULT_TIMEOUT, + AsyncStatus, NotConnected, ReadingValueCallback, SignalBackend, @@ -24,6 +25,7 @@ AttributeInfoEx, CmdArgType, CommandInfo, + DevFailed, DevState, EventType, ) @@ -182,22 +184,40 @@ async def get_w_value(self) -> T: @ensure_proper_executor async def put( self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None - ) -> None: + ) -> None or AsyncStatus: if wait: - await self._proxy.write_attribute(self._name, value) + try: + # val = await self._proxy.command_inout(self._name, value) + async def _write(): + return await self._proxy.write_attribute(self._name, value) + + task = asyncio.create_task(_write()) + await asyncio.wait_for(task, timeout) + except asyncio.TimeoutError: + raise TimeoutError(f"{self._name} attr put failed: Timeout") + except Exception as e: + raise RuntimeError(f"{self._name} attr put failed: {e}") + else: - rid = self._proxy.write_attribute_asynch(self._name, value) - if timeout: - start_time = time.time() - finished = False - while not finished: + rid = await self._proxy.write_attribute_asynch(self._name, value) + + async def wait_for_reply(rd, to): + start_time = time.time() if to else None + while True: try: - _ = await self._proxy.write_attribute_reply(rid) - finished = True - except Exception: - await asyncio.sleep(A_BIT) - if time.time() - start_time > timeout: - raise TimeoutError("Timeout while waiting for write reply") + await self._proxy.write_attribute_reply(rd) + break + except DevFailed as fail: + if fail.args[0].reason == "API_AsynReplyNotArrived": + await asyncio.sleep(A_BIT) + if to and time.time() - start_time > to: + raise TimeoutError( + f"{self._name} attr put failed:" f" Timeout" + ) + except Exception as exc: + raise RuntimeError(f"{self._name} attr put failed: {exc}") + + return AsyncStatus(wait_for_reply(rid, timeout)) # -------------------------------------------------------------------- @ensure_proper_executor @@ -358,31 +378,51 @@ async def get(self) -> T: @ensure_proper_executor async def put( self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None - ) -> None: + ) -> None or AsyncStatus: if wait: - val = await self._proxy.command_inout(self._name, value) + try: + # val = await self._proxy.command_inout(self._name, value) + async def _put(): + return await self._proxy.command_inout(self._name, value) + + task = asyncio.create_task(_put()) + val = await asyncio.wait_for(task, timeout) + self._last_reading = { + "value": val, + "timestamp": time.time(), + "alarm_severity": 0, + } + except asyncio.TimeoutError: + raise TimeoutError(f"{self._name} command failed: Timeout") + except Exception as e: + raise RuntimeError(f"{self._name} command failed: {e}") + else: - val = None rid = self._proxy.command_inout_asynch(self._name, value) - if timeout: - start_time = time.time() - finished = False - while not finished: + + async def wait_for_reply(rd, to): + reply_value = None + start_time = time.time() if to else None + while True: try: - val = await self._proxy.command_inout_reply(rid) - finished = True - except Exception: - await asyncio.sleep(A_BIT) - if time.time() - start_time > timeout: - raise TimeoutError( - "Timeout while waiting for command reply" - ) - - self._last_reading = { - "value": val, - "timestamp": time.time(), - "alarm_severity": 0, - } + reply_value = self._proxy.command_inout_reply(rd) + self._last_reading = { + "value": reply_value, + "timestamp": time.time(), + "alarm_severity": 0, + } + break + except DevFailed as fail: + if fail.args[0].reason == "API_AsynReplyNotArrived": + await asyncio.sleep(A_BIT) + if to and time.time() - start_time > to: + raise TimeoutError( + "Timeout while waiting for command reply" + ) + except Exception as exc: + raise RuntimeError(f"{self._name} command failed: {exc}") + + return AsyncStatus(wait_for_reply(rid, timeout)) # -------------------------------------------------------------------- @ensure_proper_executor @@ -552,6 +592,7 @@ def __init__( self.descriptor: Descriptor = {} # type: ignore self.polling = (True, 0.5, None, 0.1) self.support_events = False + self.status = None # -------------------------------------------------------------------- def source(self, name: str) -> str: @@ -585,7 +626,9 @@ async def connect(self, timeout: float = DEFAULT_TIMEOUT): # -------------------------------------------------------------------- async def put(self, value: Optional[T], wait=True, timeout=None): - await self.proxies[self.write_trl].put(value, wait, timeout) + self.status = None + put_status = await self.proxies[self.write_trl].put(value, wait, timeout) + self.status = put_status # -------------------------------------------------------------------- async def get_datakey(self, source: str) -> DataKey: diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 057c17de32..98680142a0 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -38,6 +38,7 @@ class TestEnum(IntEnum): TESTED_FEATURES = ["array", "limitedvalue", "justvalue"] +# -------------------------------------------------------------------- class TestDevice(Device): __test__ = False @@ -46,6 +47,7 @@ class TestDevice(Device): _justvalue = 5 _writeonly = 6 _readonly = 7 + _slow_attribute = 1.0 _limitedvalue = 3 @@ -96,11 +98,24 @@ def limitedvalue(self) -> float: def write_limitedvalue(self, value: float): self._limitedvalue = value + @attribute(dtype=float, access=AttrWriteType.WRITE) + def slow_attribute(self) -> float: + return self._slow_attribute + + def write_slow_attribute(self, value: float): + time.sleep(0.2) + self._slow_attribute = value + @command def clear(self) -> str: # self.info_stream("Received clear command") return "Received clear command" + @command + def slow_command(self) -> str: + time.sleep(0.2) + return "Completed slow command" + # -------------------------------------------------------------------- class TestReadableDevice(TangoReadableDevice): diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index 715592256e..98ea518336 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -23,7 +23,6 @@ from tango import ( CmdArgType, DevState, - GreenMode, ) from tango.asyncio import DeviceProxy from tango.asyncio_executor import ( @@ -31,22 +30,9 @@ get_global_executor, set_global_executor, ) -from tango.server import Device, command from tango.test_context import MultiDeviceTestContext -# -------------------------------------------------------------------- -class DeviceAsynch(Device): - green_mode = GreenMode.Asyncio - - @command( - polling_period=100, - ) - async def slow_command(self) -> str: - await asyncio.sleep(0.2) - return "Completed slow command" - - # -------------------------------------------------------------------- @pytest.fixture(scope="module") def tango_test_device(): @@ -65,15 +51,6 @@ def echo_device(): yield context.get_device_access("test/device/1") -# -------------------------------------------------------------------- -@pytest.fixture(scope="module") -def tango_test_device_asynch(): - with MultiDeviceTestContext( - [{"class": DeviceAsynch, "devices": [{"name": "test/device/1"}]}], process=True - ) as context: - yield context.get_device_access("test/device/1") - - # -------------------------------------------------------------------- @pytest.fixture(autouse=True) def reset_tango_asyncio(): @@ -266,12 +243,21 @@ async def test_attribute_proxy_get(device_proxy, attr): # -------------------------------------------------------------------- @pytest.mark.asyncio -@pytest.mark.parametrize("attr", ["justvalue", "array"]) -async def test_attribute_proxy_put(device_proxy, attr): +@pytest.mark.parametrize( + "attr, wait", + [("justvalue", True), ("justvalue", False), ("array", True), ("array", False)], +) +async def test_attribute_proxy_put(device_proxy, attr, wait): attr_proxy = AttributeProxy(device_proxy, attr) + old_value = await attr_proxy.get() new_value = old_value + 1 - await attr_proxy.put(new_value, wait=True) + status = await attr_proxy.put(new_value, wait=wait, timeout=0.1) + if status: + await status + else: + if not wait: + raise AssertionError("If wait is False, put should return a status object") updated_value = await attr_proxy.get() if isinstance(new_value, np.ndarray): assert np.all(updated_value == new_value) @@ -279,6 +265,17 @@ async def test_attribute_proxy_put(device_proxy, attr): assert updated_value == new_value +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize("wait", [True, False]) +async def test_attribute_proxy_put_force_timeout(device_proxy, wait): + attr_proxy = AttributeProxy(device_proxy, "slow_attribute") + with pytest.raises(TimeoutError) as exc_info: + status = await attr_proxy.put(3.0, wait=wait, timeout=0.1) + await status + assert "attr put failed" in str(exc_info.value) + + # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( @@ -441,14 +438,38 @@ async def test_command_proxy_put_wait(device_proxy): await cmd_proxy.put(None, wait=True) assert cmd_proxy._last_reading["value"] == "Received clear command" + # Force timeout + cmd_proxy = CommandProxy(device_proxy, "slow_command") + cmd_proxy._last_reading = None + with pytest.raises(TimeoutError) as exc_info: + await cmd_proxy.put(None, wait=True, timeout=0.1) + assert "command failed" in str(exc_info.value) + # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_command_proxy_put_nowait(device_proxy_asynch): - cmd_proxy = CommandProxy(device_proxy_asynch, "slow_command") +async def test_command_proxy_put_nowait(device_proxy): + cmd_proxy = CommandProxy(device_proxy, "slow_command") + + # Reply before timeout + cmd_proxy._last_reading = None + status = await cmd_proxy.put(None, wait=False, timeout=0.5) + assert cmd_proxy._last_reading is None + await status + assert cmd_proxy._last_reading["value"] == "Completed slow command" + + # Timeout + cmd_proxy._last_reading = None + status = await cmd_proxy.put(None, wait=False, timeout=0.1) + with pytest.raises(TimeoutError) as exc_info: + await status + assert str(exc_info.value) == "Timeout while waiting for command reply" + # No timeout cmd_proxy._last_reading = None - await cmd_proxy.put(None, wait=False, timeout=0.5) + status = await cmd_proxy.put(None, wait=False) + assert cmd_proxy._last_reading is None + await status assert cmd_proxy._last_reading["value"] == "Completed slow command" From 31303b51580bf2b72951a6d04455c80a862ee342 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 14 Aug 2024 15:18:41 +0200 Subject: [PATCH 084/141] call to set_name required after registering signals --- src/ophyd_async/tango/base_devices/base_device.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index f0c8189c3a..8a6d539c2c 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -50,5 +50,7 @@ async def closure(): # If a register_signals method is defined, call it if hasattr(self, "register_signals"): self.register_signals() + # set_name must be called again to propagate the new signal names + self.set_name(self.name) await super().connect(mock=mock, timeout=timeout) From 06f8956c592d603ffeba424ce5361b14710e2c3e Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 14 Aug 2024 15:19:24 +0200 Subject: [PATCH 085/141] Improving coverage to base device --- tests/tango/test_base_device.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 98680142a0..397a141185 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -123,10 +123,12 @@ class TestReadableDevice(TangoReadableDevice): def __init__(self, trl: str, name="") -> None: self.trl = trl + TangoReadableDevice.__init__(self, trl, name) + + def register_signals(self): for feature in TESTED_FEATURES: with self.add_children_as_readables(): - setattr(self, feature, tango_signal_auto(None, f"{trl}/{feature}")) - TangoReadableDevice.__init__(self, trl, name) + setattr(self, feature, tango_signal_auto(None, f"{self.trl}/{feature}")) # -------------------------------------------------------------------- From 1193e4312d54366b1bc2d61f8a5c2086fca1173a Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 14 Aug 2024 16:15:49 +0200 Subject: [PATCH 086/141] Added get_w_value to CommandProxy so that locate() works when it is the backend to SignalRW --- .../tango/_backend/_tango_transport.py | 20 +++++++++----- tests/tango/test_base_device.py | 4 +++ tests/tango/test_tango_signals.py | 27 ++++++++++++++----- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 65e597dccb..03c1843f80 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -228,11 +228,10 @@ async def get_config(self) -> AttributeInfoEx: @ensure_proper_executor async def get_reading(self) -> Reading: attr = await self._proxy.read_attribute(self._name) - return { - "value": attr.value, - "timestamp": attr.time.totime(), - "alarm_severity": attr.quality, - } + reading = Reading( + value=attr.value, timestamp=attr.time.totime(), alarm_severity=attr.quality + ) + return reading # -------------------------------------------------------------------- def has_subscription(self) -> bool: @@ -374,6 +373,10 @@ class CommandProxy(TangoProxy): async def get(self) -> T: return self._last_reading["value"] + # -------------------------------------------------------------------- + async def get_w_value(self) -> T: + return self._last_reading["value"] + # -------------------------------------------------------------------- @ensure_proper_executor async def put( @@ -431,7 +434,12 @@ async def get_config(self) -> CommandInfo: # -------------------------------------------------------------------- async def get_reading(self) -> Reading: - return self._last_reading + reading = Reading( + value=self._last_reading["value"], + timestamp=self._last_reading["timestamp"], + alarm_severity=self._last_reading["alarm_severity"], + ) + return reading # -------------------------------------------------------------------- def set_polling( diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 397a141185..9ac1d0799c 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -116,6 +116,10 @@ def slow_command(self) -> str: time.sleep(0.2) return "Completed slow command" + @command + def echo(self, value: str) -> str: + return value + # -------------------------------------------------------------------- class TestReadableDevice(TangoReadableDevice): diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index 2ebc487415..7faf8f4a32 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -584,7 +584,13 @@ async def test_tango_signal_x(tango_test_device: str, use_proxy: bool): @pytest.mark.parametrize("use_proxy", [True, False]) @pytest.mark.parametrize( "attr_type", - [("justvalue", int), ("writeonly", int), ("readonly", int), ("clear", None)], + [ + ("justvalue", int), + ("writeonly", int), + ("readonly", int), + ("clear", None), + ("echo", str), + ], ) async def test_tango_signal_auto( tango_test_device: str, attr_type: tuple[str, Type[T]], use_proxy: bool @@ -601,12 +607,21 @@ async def test_tango_signal_auto( await signal.connect() if isinstance(signal, SignalRW): - reading = await signal.read() - assert_close(reading[""]["value"], 5) - await signal.set(8, timeout=timeout) + datatype = signal._backend.datatype + if datatype is int: + new_value = choice([1, 2, 3, 4, 5]) + elif datatype is float: + new_value = choice([1.1, 2.2, 3.3, 4.4, 5.5]) + elif datatype is str: + new_value = choice(["aaa", "bbb", "ccc"]) + elif datatype is np.ndarray: + new_value = np.array([choice([1, 2, 3, 4, 5]), choice([1, 2, 3, 4, 5])]) + else: + new_value = None + await signal.set(new_value, timeout=timeout) location = await signal.locate() - assert_close(location["setpoint"], 8) - assert_close(location["readback"], 8) + assert_close(location["setpoint"], new_value) + assert_close(location["readback"], new_value) return if isinstance(signal, SignalR): From ad7e93f8cd204ff6813e2604eba4a7e400348145 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 14 Aug 2024 16:29:57 +0200 Subject: [PATCH 087/141] Improved coverage for auto signals. Moved check for nonexistend attributes/commands --- src/ophyd_async/tango/signal/signal.py | 7 +++++-- tests/tango/test_tango_signals.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 3c3445cd54..d4b6151c33 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -143,6 +143,11 @@ def tango_signal_auto( device_trl, tr_name = full_trl.rsplit("/", 1) syn_proxy = SyncDeviceProxy(device_trl) backend = _make_backend(datatype, full_trl, full_trl, device_proxy) + + if tr_name not in syn_proxy.get_attribute_list(): + if tr_name not in syn_proxy.get_command_list(): + raise RuntimeError(f"Cannot find {tr_name} in {device_trl}") + if tr_name in syn_proxy.get_attribute_list(): config = syn_proxy.get_attribute_config(tr_name) if config.writable in [AttrWriteType.READ_WRITE, AttrWriteType.READ_WITH_WRITE]: @@ -158,8 +163,6 @@ def tango_signal_auto( return SignalX(backend) elif config.out_type != CmdArgType.DevVoid: return SignalRW(backend) - else: - return SignalX(backend) if tr_name in device_proxy.get_pipe_list(): raise NotImplementedError("Pipes are not supported") diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index 7faf8f4a32..4cef367995 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -590,6 +590,7 @@ async def test_tango_signal_x(tango_test_device: str, use_proxy: bool): ("readonly", int), ("clear", None), ("echo", str), + ("nonexistant", int), ], ) async def test_tango_signal_auto( @@ -599,6 +600,16 @@ async def test_tango_signal_auto( attr, py_type = attr_type timeout = 0.1 + if attr == "nonexistant": + with pytest.raises(RuntimeError) as exc_info: + signal = tango_signal_auto( + datatype=py_type, + full_trl=tango_test_device + "/" + attr, + device_proxy=proxy, + ) + await signal.connect() + assert "Cannot find" in str(exc_info.value) + return signal = tango_signal_auto( datatype=py_type, full_trl=tango_test_device + "/" + attr, From c2f88efb43052d555c594853c291bd5d003b17e1 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 15 Aug 2024 08:23:07 +0200 Subject: [PATCH 088/141] Removed redundant check for tr_name --- src/ophyd_async/tango/signal/signal.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index d4b6151c33..94c04932ce 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -166,5 +166,3 @@ def tango_signal_auto( if tr_name in device_proxy.get_pipe_list(): raise NotImplementedError("Pipes are not supported") - - raise RuntimeError(f"Cannot find {tr_name} in {device_trl}") From a113923c605bd2bfcdfe093cc8ed8ceaca688585 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 15 Aug 2024 09:17:08 +0200 Subject: [PATCH 089/141] Improved poll testing coverage and added tests for arrays and strings --- .../tango/_backend/_tango_transport.py | 23 ++++++++++----- tests/tango/test_base_device.py | 18 ++++++++++++ tests/tango/test_tango_transport.py | 29 +++++++++++++++++-- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 03c1843f80..e2b8651eb9 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -297,7 +297,7 @@ async def poll(self): if self._callback is not None: self._callback(last_reading, last_reading["value"]) except Exception as e: - raise RuntimeError("Could not poll the attribute") from e + raise RuntimeError(f"Could not poll the attribute: {e}") try: # If the value is a number, we can check for changes @@ -306,7 +306,6 @@ async def poll(self): await asyncio.sleep(self._polling_period) reading = await self.get_reading() if reading is None or reading["value"] is None: - reading = last_reading.copy() continue diff = abs(reading["value"] - last_reading["value"]) if self._abs_change is not None and diff >= abs(self._abs_change): @@ -339,13 +338,23 @@ async def poll(self): reading = await self.get_reading() if reading is None or reading["value"] is None: continue - last_reading = reading.copy() - if self._callback is not None: - self._callback(reading, reading["value"]) + if isinstance(reading["value"], np.ndarray): + if not np.array_equal( + reading["value"], last_reading["value"] + ): + if self._callback is not None: + self._callback(reading, reading["value"]) + else: + break else: - break + if reading["value"] != last_reading["value"]: + if self._callback is not None: + self._callback(reading, reading["value"]) + else: + break + last_reading = reading.copy() except Exception as e: - raise RuntimeError("Could not poll the attribute") from e + raise RuntimeError(f"Could not poll the attribute: {e}") # -------------------------------------------------------------------- def set_polling( diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 9ac1d0799c..1aedcfd40f 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -49,8 +49,26 @@ class TestDevice(Device): _readonly = 7 _slow_attribute = 1.0 + _floatvalue = 1.0 + + _label = "Test Device" + _limitedvalue = 3 + @attribute(dtype=str, access=AttrWriteType.READ_WRITE) + def label(self): + return self._label + + def write_label(self, value: str): + self._label = value + + @attribute(dtype=float, access=AttrWriteType.READ_WRITE) + def floatvalue(self): + return self._floatvalue + + def write_floatvalue(self, value: float): + self._floatvalue = value + @attribute(dtype=int, access=AttrWriteType.READ_WRITE, polling_period=100) def justvalue(self): return self._justvalue diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index 98ea518336..642d4f897a 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -376,7 +376,7 @@ def test_attribute_set_polling(device_proxy): # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_attribute_poll(device_proxy): - attr_proxy = AttributeProxy(device_proxy, "justvalue") + attr_proxy = AttributeProxy(device_proxy, "floatvalue") def callback(reading, value): nonlocal val @@ -425,10 +425,35 @@ def bad_callback(): # Test polling with bad callback attr_proxy.subscribe_callback(bad_callback) await asyncio.sleep(0.2) - assert str(attr_proxy._poll_task.exception()) == "Could not poll the attribute" + assert "Could not poll the attribute" in str(attr_proxy._poll_task.exception()) attr_proxy.unsubscribe_callback() +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize("attr", ["array", "label"]) +async def test_attribute_poll_stringsandarrays(device_proxy, attr): + attr_proxy = AttributeProxy(device_proxy, attr) + + def callback(reading, value): + nonlocal val + val = value + + val = None + attr_proxy.set_polling(True, 0.1) + attr_proxy.subscribe_callback(callback) + await asyncio.sleep(0.2) + assert val is not None + if isinstance(val, np.ndarray): + await attr_proxy.put(np.array([[2, 3, 4], [5, 6, 7]])) + await asyncio.sleep(0.5) + assert np.all(val == np.array([[2, 3, 4], [5, 6, 7]])) + if isinstance(val, str): + await attr_proxy.put("new label") + await asyncio.sleep(0.5) + assert val == "new label" + + # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_command_proxy_put_wait(device_proxy): From 3aefd072493cde588f3228de018626c585d6b23e Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 15 Aug 2024 09:21:51 +0200 Subject: [PATCH 090/141] Added tests for polling exceptions for nonexistent attributes and commands --- tests/tango/test_tango_transport.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index 642d4f897a..a8965346a1 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -454,6 +454,32 @@ def callback(reading, value): assert val == "new label" +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_attribute_poll_exceptions(device_proxy): + # Try to poll a non-existent attribute + attr_proxy = AttributeProxy(device_proxy, "nonexistent") + attr_proxy.set_polling(True, 0.1) + + def callback(reading, value): + pass + + attr_proxy.subscribe_callback(callback) + await asyncio.sleep(0.2) + assert "Could not poll the attribute" in str(attr_proxy._poll_task.exception()) + + # Try to poll a command + attr_proxy = AttributeProxy(device_proxy, "clear") + attr_proxy.set_polling(True, 0.1) + + def callback(reading, value): + pass + + attr_proxy.subscribe_callback(callback) + await asyncio.sleep(0.2) + assert "Could not poll the attribute" in str(attr_proxy._poll_task.exception()) + + # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_command_proxy_put_wait(device_proxy): From 4fc9c9f4c9f0303163a8d64193a0d7fb7385909e Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 15 Aug 2024 09:47:03 +0200 Subject: [PATCH 091/141] Added test for a TangoTransport with separate read/write TRLs --- tests/tango/test_base_device.py | 15 +++++++++++++++ tests/tango/test_tango_transport.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 1aedcfd40f..6dceca0cfd 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -51,10 +51,25 @@ class TestDevice(Device): _floatvalue = 1.0 + _readback = 1.0 + _setpoint = 1.0 + _label = "Test Device" _limitedvalue = 3 + @attribute(dtype=float, access=AttrWriteType.READ) + def readback(self): + return self._readback + + @attribute(dtype=float, access=AttrWriteType.WRITE) + def setpoint(self): + return self._setpoint + + def write_setpoint(self, value: float): + self._setpoint = value + self._readback = value + @attribute(dtype=str, access=AttrWriteType.READ_WRITE) def label(self): return self._label diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index a8965346a1..7fdc60cf46 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -14,6 +14,7 @@ from ophyd_async.tango._backend._tango_transport import ( AttributeProxy, CommandProxy, + TangoTransport, ensure_proper_executor, get_dtype_extended, get_python_type, @@ -674,3 +675,31 @@ async def test_tango_transport_set_polling(transport): async def test_tango_transport_allow_events(transport, allow): transport.allow_events(allow) assert transport.support_events == allow + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_read_and_write_trl(device_proxy): + trl = device_proxy.dev_name() + read_trl = trl + "/" + "readback" + write_trl = trl + "/" + "setpoint" + + # Test with existing proxy + transport = TangoTransport(float, read_trl, write_trl, device_proxy) + await transport.connect() + reading = await transport.get_reading() + initial_value = reading["value"] + new_value = initial_value + 1.0 + await transport.put(new_value) + updated_value = await transport.get_value() + assert updated_value == new_value + + # Without pre-existing proxy + transport = TangoTransport(float, read_trl, write_trl, None) + await transport.connect() + reading = await transport.get_reading() + initial_value = reading["value"] + new_value = initial_value + 1.0 + await transport.put(new_value) + updated_value = await transport.get_value() + assert updated_value == new_value From 31e59a120df0f878c0b2cf5869a4662d99a4bbde Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 15 Aug 2024 10:04:03 +0200 Subject: [PATCH 092/141] Added tests to catch device failed exceptions when putting. --- .../tango/_backend/_tango_transport.py | 30 +++++++++++-------- tests/tango/test_base_device.py | 11 +++++++ tests/tango/test_tango_transport.py | 21 +++++++++++++ 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index e2b8651eb9..583b37fe4f 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -187,7 +187,7 @@ async def put( ) -> None or AsyncStatus: if wait: try: - # val = await self._proxy.command_inout(self._name, value) + async def _write(): return await self._proxy.write_attribute(self._name, value) @@ -195,8 +195,8 @@ async def _write(): await asyncio.wait_for(task, timeout) except asyncio.TimeoutError: raise TimeoutError(f"{self._name} attr put failed: Timeout") - except Exception as e: - raise RuntimeError(f"{self._name} attr put failed: {e}") + except DevFailed as e: + raise RuntimeError(f"{self._name} device failure: {e.args[0].desc}") else: rid = await self._proxy.write_attribute_asynch(self._name, value) @@ -207,15 +207,17 @@ async def wait_for_reply(rd, to): try: await self._proxy.write_attribute_reply(rd) break - except DevFailed as fail: - if fail.args[0].reason == "API_AsynReplyNotArrived": + except DevFailed as exc: + if exc.args[0].reason == "API_AsynReplyNotArrived": await asyncio.sleep(A_BIT) if to and time.time() - start_time > to: raise TimeoutError( f"{self._name} attr put failed:" f" Timeout" ) - except Exception as exc: - raise RuntimeError(f"{self._name} attr put failed: {exc}") + else: + raise RuntimeError( + f"{self._name} device failure:" f" {exc.args[0].desc}" + ) return AsyncStatus(wait_for_reply(rid, timeout)) @@ -406,8 +408,8 @@ async def _put(): } except asyncio.TimeoutError: raise TimeoutError(f"{self._name} command failed: Timeout") - except Exception as e: - raise RuntimeError(f"{self._name} command failed: {e}") + except DevFailed as e: + raise RuntimeError(f"{self._name} device failure: {e.args[0].desc}") else: rid = self._proxy.command_inout_asynch(self._name, value) @@ -424,15 +426,17 @@ async def wait_for_reply(rd, to): "alarm_severity": 0, } break - except DevFailed as fail: - if fail.args[0].reason == "API_AsynReplyNotArrived": + except DevFailed as e: + if e.args[0].reason == "API_AsynReplyNotArrived": await asyncio.sleep(A_BIT) if to and time.time() - start_time > to: raise TimeoutError( "Timeout while waiting for command reply" ) - except Exception as exc: - raise RuntimeError(f"{self._name} command failed: {exc}") + else: + raise RuntimeError( + f"{self._name} device failure:" f" {e.args[0].desc}" + ) return AsyncStatus(wait_for_reply(rid, timeout)) diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 6dceca0cfd..d26b535963 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -139,6 +139,13 @@ def write_slow_attribute(self, value: float): time.sleep(0.2) self._slow_attribute = value + @attribute(dtype=float, access=AttrWriteType.READ_WRITE) + def raise_exception_attr(self) -> float: + raise + + def write_raise_exception_attr(self, value: float): + raise + @command def clear(self) -> str: # self.info_stream("Received clear command") @@ -153,6 +160,10 @@ def slow_command(self) -> str: def echo(self, value: str) -> str: return value + @command + def raise_exception_cmd(self): + raise + # -------------------------------------------------------------------- class TestReadableDevice(TangoReadableDevice): diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index 7fdc60cf46..957f088515 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -277,6 +277,17 @@ async def test_attribute_proxy_put_force_timeout(device_proxy, wait): assert "attr put failed" in str(exc_info.value) +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize("wait", [True, False]) +async def test_attribute_proxy_put_exceptions(device_proxy, wait): + attr_proxy = AttributeProxy(device_proxy, "raise_exception_attr") + with pytest.raises(RuntimeError) as exc_info: + status = await attr_proxy.put(3.0, wait=wait) + await status + assert "device failure" in str(exc_info.value) + + # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( @@ -525,6 +536,16 @@ async def test_command_proxy_put_nowait(device_proxy): assert cmd_proxy._last_reading["value"] == "Completed slow command" +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize("wait", [True, False]) +async def test_command_proxy_put_exceptions(device_proxy, wait): + cmd_proxy = CommandProxy(device_proxy, "raise_exception_cmd") + with pytest.raises(RuntimeError) as exc_info: + await cmd_proxy.put(None, wait=True) + assert "device failure" in str(exc_info.value) + + # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_command_get(device_proxy): From 969db94ee56a0a18b7ab50867051be48d71c91e3 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 15 Aug 2024 10:52:00 +0200 Subject: [PATCH 093/141] Improved coverage of TangoTransport exceptions and added protection from non-callable callbacks --- .../tango/_backend/_tango_transport.py | 17 +++--- tests/tango/test_tango_transport.py | 57 ++++++++++++++++++- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 583b37fe4f..6408bfb0bc 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -242,6 +242,10 @@ def has_subscription(self) -> bool: # -------------------------------------------------------------------- def subscribe_callback(self, callback: Optional[ReadingValueCallback]): # If the attribute supports events, then we can subscribe to them + # If the callback is not a callable, then we raise an error + if callback is not None and not callable(callback): + raise RuntimeError("Callback must be a callable") + self._callback = callback if self.support_events: """add user callback to CHANGE event subscription""" @@ -680,16 +684,13 @@ def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: ) if callback: - assert not self.proxies[ - self.read_trl - ].has_subscription(), "Cannot set a callback when one is already set" try: + assert not self.proxies[self.read_trl].has_subscription() self.proxies[self.read_trl].subscribe_callback(callback) - except AssertionError or RuntimeError: - raise RuntimeError( - f"Cannot set event for {self.read_trl}. " - f"This signal should be used only as non-cached!" - ) + except AssertionError: + raise RuntimeError("Cannot set a callback when one is already set") + except RuntimeError as exc: + raise RuntimeError(f"Cannot set callback for {self.read_trl}. {exc}") else: self.proxies[self.read_trl].unsubscribe_callback() diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index 957f088515..77439ad607 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -669,14 +669,36 @@ def callback(reading, value): nonlocal val val = value + # Correct usage transport.set_callback(callback) current_value = await transport.get_value() new_value = current_value + 2 await transport.put(new_value) await asyncio.sleep(0.1) - transport.set_callback(None) assert val == new_value + # Try to add second callback + with pytest.raises(RuntimeError) as exc_info: + transport.set_callback(callback) + assert "Cannot set a callback when one is already set" + + transport.set_callback(None) + + # Try to add a callback to a non-callable proxy + transport.allow_events(False) + transport.set_polling(False) + with pytest.raises(RuntimeError) as exc_info: + transport.set_callback(callback) + assert "Cannot set event" in str(exc_info.value) + + # Try to add a non-callable callback + transport.allow_events(True) + transport.set_callback(None) + with pytest.raises(RuntimeError) as exc_info: + transport.set_callback(1) + print(exc_info.value) + assert "Callback must be a callable" in str(exc_info.value) + # -------------------------------------------------------------------- @pytest.mark.asyncio @@ -724,3 +746,36 @@ async def test_tango_transport_read_and_write_trl(device_proxy): await transport.put(new_value) updated_value = await transport.get_value() assert updated_value == new_value + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_read_only_trl(device_proxy): + trl = device_proxy.dev_name() + read_trl = trl + "/" + "readonly" + + # Test with existing proxy + transport = TangoTransport(int, read_trl, read_trl, device_proxy) + await transport.connect() + with pytest.raises(RuntimeError) as exc_info: + await transport.put(1) + assert "is not writable" in str(exc_info.value) + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_nonexistent_trl(device_proxy): + trl = device_proxy.dev_name() + nonexistent_trl = trl + "/" + "nonexistent" + + # Test with existing proxy + transport = TangoTransport(int, nonexistent_trl, nonexistent_trl, device_proxy) + with pytest.raises(RuntimeError) as exc_info: + await transport.connect() + assert "cannot be found" in str(exc_info.value) + + # Without pre-existing proxy + transport = TangoTransport(int, nonexistent_trl, nonexistent_trl, None) + with pytest.raises(RuntimeError) as exc_info: + await transport.connect() + assert "cannot be found" in str(exc_info.value) From 6d41773c79cabc0d0bf928a16d6ea9c1d9bf07bd Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 19 Aug 2024 10:17:39 +0200 Subject: [PATCH 094/141] Added exhaustive testing of automatic signals including cases where a proxy and datatype and unspecified at initialization --- src/ophyd_async/tango/signal/signal.py | 20 +- tests/tango/test_tango_signals.py | 256 +++++++++++++++++++------ 2 files changed, 205 insertions(+), 71 deletions(-) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 94c04932ce..c308aebaef 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -138,11 +138,15 @@ def tango_signal_x( # -------------------------------------------------------------------- def tango_signal_auto( - datatype: Type[T], full_trl: str, device_proxy: Optional[DeviceProxy] = None + datatype: Type[T], + trl: str, + device_proxy: Optional[DeviceProxy] = None, + timeout: float = DEFAULT_TIMEOUT, + name: str = "", ) -> Union[SignalW, SignalX, SignalR, SignalRW]: - device_trl, tr_name = full_trl.rsplit("/", 1) + device_trl, tr_name = trl.rsplit("/", 1) syn_proxy = SyncDeviceProxy(device_trl) - backend = _make_backend(datatype, full_trl, full_trl, device_proxy) + backend = _make_backend(datatype, trl, trl, device_proxy) if tr_name not in syn_proxy.get_attribute_list(): if tr_name not in syn_proxy.get_command_list(): @@ -151,18 +155,18 @@ def tango_signal_auto( if tr_name in syn_proxy.get_attribute_list(): config = syn_proxy.get_attribute_config(tr_name) if config.writable in [AttrWriteType.READ_WRITE, AttrWriteType.READ_WITH_WRITE]: - return SignalRW(backend) + return SignalRW(backend, timeout=timeout, name=name) elif config.writable == AttrWriteType.READ: - return SignalR(backend) + return SignalR(backend, timeout=timeout, name=name) else: - return SignalW(backend) + return SignalW(backend, timeout=timeout, name=name) if tr_name in syn_proxy.get_command_list(): config = syn_proxy.get_command_config(tr_name) if config.in_type == CmdArgType.DevVoid: - return SignalX(backend) + return SignalX(backend, timeout=timeout, name=name) elif config.out_type != CmdArgType.DevVoid: - return SignalRW(backend) + return SignalRW(backend, timeout=timeout, name=name) if tr_name in device_proxy.get_pipe_list(): raise NotImplementedError("Pipes are not supported") diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index 4cef367995..1cb84a5037 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -11,7 +11,7 @@ from bluesky.protocols import Reading from test_base_device import TestDevice -from ophyd_async.core import SignalBackend, SignalR, SignalRW, SignalW, SignalX, T +from ophyd_async.core import SignalBackend, SignalRW, SignalX, T from ophyd_async.tango._backend import TangoTransport from ophyd_async.tango.signal import ( tango_signal_auto, @@ -561,10 +561,48 @@ async def test_tango_signal_rw( assert_close(location["readback"], put_value) +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize( + "pv, tango_type, d_format, py_type, initial_value, put_value", + COMMANDS_SET, + ids=[x[0] for x in COMMANDS_SET], +) +async def test_tango_signal_x( + echo_device: str, + pv: str, + tango_type: str, + d_format: AttrDataFormat, + py_type: Type[T], + initial_value: T, + put_value: T, +): + source = echo_device + "/" + pv + timeout = 0.1 + signal = tango_signal_auto( + datatype=py_type, + trl=source, + device_proxy=None, + name="test_signal", + timeout=timeout, + ) + await signal.connect() + assert signal + reading = await signal.read() + assert reading["test_signal"]["value"] is None + + await signal.set(put_value, wait=True, timeout=0.1) + reading = await signal.read() + value = reading["test_signal"]["value"] + if isinstance(value, np.ndarray): + value = value.tolist() + assert_close(value, put_value) + + # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize("use_proxy", [True, False]) -async def test_tango_signal_x(tango_test_device: str, use_proxy: bool): +async def test_tango_signal_x_none(tango_test_device: str, use_proxy: bool): proxy = await DeviceProxy(tango_test_device) if use_proxy else None timeout = 0.1 signal = tango_signal_x( @@ -581,75 +619,167 @@ async def test_tango_signal_x(tango_test_device: str, use_proxy: bool): # -------------------------------------------------------------------- @pytest.mark.asyncio -@pytest.mark.parametrize("use_proxy", [True, False]) @pytest.mark.parametrize( - "attr_type", + "pv, tango_type, d_format, py_type, initial_value, put_value, use_dtype, use_proxy", [ - ("justvalue", int), - ("writeonly", int), - ("readonly", int), - ("clear", None), - ("echo", str), - ("nonexistant", int), + ( + pv, + tango_type, + d_format, + py_type, + initial_value, + put_value, + use_dtype, + use_proxy, + ) + for ( + pv, + tango_type, + d_format, + py_type, + initial_value, + put_value, + ) in ATTRIBUTES_SET + for use_dtype in [True, False] + for use_proxy in [True, False] + ], + ids=[ + f"{x[0]}_{use_dtype}_{use_proxy}" + for x in ATTRIBUTES_SET + for use_dtype in [True, False] + for use_proxy in [True, False] ], ) -async def test_tango_signal_auto( - tango_test_device: str, attr_type: tuple[str, Type[T]], use_proxy: bool +async def test_tango_signal_auto_attrs( + echo_device: str, + pv: str, + tango_type: str, + d_format: AttrDataFormat, + py_type: Type[T], + initial_value: T, + put_value: T, + use_dtype: bool, + use_proxy: bool, ): - proxy = await DeviceProxy(tango_test_device) if use_proxy else None - attr, py_type = attr_type + await prepare_device(echo_device, pv, initial_value) + source = echo_device + "/" + pv + proxy = await DeviceProxy(echo_device) if use_proxy else None + timeout = 0.1 + async def _test_signal(dtype, proxy): + signal = tango_signal_auto( + datatype=dtype, + trl=source, + device_proxy=proxy, + timeout=timeout, + name="test_signal", + ) + assert type(signal) is SignalRW + await signal.connect() + reading = await signal.read() + value = reading["test_signal"]["value"] + if isinstance(value, np.ndarray): + value = value.tolist() + assert_close(value, initial_value) + + await signal.set(put_value, wait=True, timeout=timeout) + reading = await signal.read() + value = reading["test_signal"]["value"] + if isinstance(value, np.ndarray): + value = value.tolist() + assert_close(value, put_value) + + dtype = py_type if use_dtype else None + await _test_signal(dtype, proxy) + + +# -------------------------------------------------------------------- + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "pv, tango_type, d_format, py_type, initial_value, put_value, use_dtype, use_proxy", + [ + ( + pv, + tango_type, + d_format, + py_type, + initial_value, + put_value, + use_dtype, + use_proxy, + ) + for ( + pv, + tango_type, + d_format, + py_type, + initial_value, + put_value, + ) in COMMANDS_SET + for use_dtype in [True, False] + for use_proxy in [True, False] + ], + ids=[ + f"{x[0]}_{use_dtype}_{use_proxy}" + for x in COMMANDS_SET + for use_dtype in [True, False] + for use_proxy in [True, False] + ], +) +async def test_tango_signal_auto_cmds( + echo_device: str, + pv: str, + tango_type: str, + d_format: AttrDataFormat, + py_type: Type[T], + initial_value: T, + put_value: T, + use_dtype: bool, + use_proxy: bool, +): + source = echo_device + "/" + pv timeout = 0.1 - if attr == "nonexistant": - with pytest.raises(RuntimeError) as exc_info: - signal = tango_signal_auto( - datatype=py_type, - full_trl=tango_test_device + "/" + attr, - device_proxy=proxy, - ) - await signal.connect() - assert "Cannot find" in str(exc_info.value) - return + + async def _test_signal(dtype, proxy): + signal = tango_signal_auto( + datatype=dtype, + trl=source, + device_proxy=proxy, + name="test_signal", + timeout=timeout, + ) + # Ophyd SignalX does not support types + assert type(signal) is SignalRW + await signal.connect() + assert signal + reading = await signal.read() + assert reading["test_signal"]["value"] is None + + await signal.set(put_value, wait=True, timeout=0.1) + reading = await signal.read() + value = reading["test_signal"]["value"] + if isinstance(value, np.ndarray): + value = value.tolist() + assert_close(value, put_value) + + proxy = await DeviceProxy(echo_device) if use_proxy else None + dtype = py_type if use_dtype else None + await _test_signal(dtype, proxy) + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize("use_proxy", [True, False]) +async def test_tango_signal_auto_cmds_none(tango_test_device: str, use_proxy: bool): + proxy = await DeviceProxy(tango_test_device) if use_proxy else None signal = tango_signal_auto( - datatype=py_type, - full_trl=tango_test_device + "/" + attr, + datatype=None, + trl=tango_test_device + "/" + "clear", device_proxy=proxy, ) + assert type(signal) is SignalX await signal.connect() - - if isinstance(signal, SignalRW): - datatype = signal._backend.datatype - if datatype is int: - new_value = choice([1, 2, 3, 4, 5]) - elif datatype is float: - new_value = choice([1.1, 2.2, 3.3, 4.4, 5.5]) - elif datatype is str: - new_value = choice(["aaa", "bbb", "ccc"]) - elif datatype is np.ndarray: - new_value = np.array([choice([1, 2, 3, 4, 5]), choice([1, 2, 3, 4, 5])]) - else: - new_value = None - await signal.set(new_value, timeout=timeout) - location = await signal.locate() - assert_close(location["setpoint"], new_value) - assert_close(location["readback"], new_value) - return - - if isinstance(signal, SignalR): - reading = await signal.read() - assert reading[""]["value"] == 7 - return - - if isinstance(signal, SignalW): - status = signal.set(1, timeout=timeout) - await status - assert status.done is True and status.success is True - return - - if isinstance(signal, SignalX): - status = signal.trigger() - await status - assert status.done is True and status.success is True - return - - assert False + assert signal + await signal.trigger(wait=True) From 648aa5c15af621e0010b67e5f49567189d0aad74 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 20 Aug 2024 15:57:12 +0200 Subject: [PATCH 095/141] Added methods for tango_signals to automatically infer the datatype if not passed as an argument. --- src/ophyd_async/tango/signal/signal.py | 39 +++++++- tests/tango/test_tango_signals.py | 124 +++++++++++++++---------- 2 files changed, 109 insertions(+), 54 deletions(-) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index c308aebaef..94152c488e 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -2,11 +2,14 @@ from __future__ import annotations +from enum import Enum, IntEnum from typing import Optional, Type, Union +import numpy.typing as npt + from ophyd_async.core import DEFAULT_TIMEOUT, SignalR, SignalRW, SignalW, SignalX, T -from ophyd_async.tango._backend import TangoTransport -from tango import AttrWriteType, CmdArgType +from ophyd_async.tango._backend._tango_transport import TangoTransport, get_python_type +from tango import AttrDataFormat, AttrWriteType, CmdArgType, DevState from tango import DeviceProxy as SyncDeviceProxy from tango.asyncio import DeviceProxy @@ -138,7 +141,8 @@ def tango_signal_x( # -------------------------------------------------------------------- def tango_signal_auto( - datatype: Type[T], + datatype: Optional[Type[T]] = None, + *, trl: str, device_proxy: Optional[DeviceProxy] = None, timeout: float = DEFAULT_TIMEOUT, @@ -146,6 +150,10 @@ def tango_signal_auto( ) -> Union[SignalW, SignalX, SignalR, SignalRW]: device_trl, tr_name = trl.rsplit("/", 1) syn_proxy = SyncDeviceProxy(device_trl) + + if datatype is None: + datatype = infer_python_type(trl) + backend = _make_backend(datatype, trl, trl, device_proxy) if tr_name not in syn_proxy.get_attribute_list(): @@ -170,3 +178,28 @@ def tango_signal_auto( if tr_name in device_proxy.get_pipe_list(): raise NotImplementedError("Pipes are not supported") + + +# -------------------------------------------------------------------- +def infer_python_type(trl: str): + device_trl, tr_name = trl.rsplit("/", 1) + syn_proxy = SyncDeviceProxy(device_trl) + + if tr_name in syn_proxy.get_command_list(): + config = syn_proxy.get_command_config(tr_name) + isarray, py_type, _ = get_python_type(config.in_type) + elif tr_name in syn_proxy.get_attribute_list(): + config = syn_proxy.get_attribute_config(tr_name) + isarray, py_type, _ = get_python_type(config.data_type) + if py_type is Enum: + enum_dict = {label: i for i, label in enumerate(config.enum_labels)} + py_type = IntEnum("TangoEnum", enum_dict) + if config.data_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]: + isarray = True + else: + raise RuntimeError(f"Cannot find {tr_name} in {device_trl}") + + if py_type is CmdArgType.DevState: + py_type = DevState + + return npt.NDArray[py_type] if isarray else py_type diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index 1cb84a5037..3d5eb0e19b 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -411,9 +411,18 @@ async def test_backend_get_put_monitor_cmd( # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( - "pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy", + "pv, tango_type, d_format, py_type, initial_value, put_value, use_dtype, use_proxy", [ - (pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy) + ( + pv, + tango_type, + d_format, + py_type, + initial_value, + put_value, + use_dtype, + use_proxy, + ) for ( pv, tango_type, @@ -422,9 +431,15 @@ async def test_backend_get_put_monitor_cmd( initial_value, put_value, ) in ATTRIBUTES_SET + for use_dtype in [True, False] + for use_proxy in [True, False] + ], + ids=[ + f"{x[0]}_{use_dtype}_{use_proxy}" + for x in ATTRIBUTES_SET + for use_dtype in [True, False] for use_proxy in [True, False] ], - ids=[f"{x[0]}_{use_proxy}" for x in ATTRIBUTES_SET for use_proxy in [True, False]], ) async def test_tango_signal_r( echo_device: str, @@ -434,11 +449,13 @@ async def test_tango_signal_r( py_type: Type[T], initial_value: T, put_value: T, + use_dtype: bool, use_proxy: bool, ): await prepare_device(echo_device, pv, initial_value) source = echo_device + "/" + pv proxy = await DeviceProxy(echo_device) if use_proxy else None + py_type = py_type if use_dtype else None timeout = 0.1 signal = tango_signal_r( @@ -456,9 +473,18 @@ async def test_tango_signal_r( # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( - "pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy", + "pv, tango_type, d_format, py_type, initial_value, put_value, use_dtype, use_proxy", [ - (pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy) + ( + pv, + tango_type, + d_format, + py_type, + initial_value, + put_value, + use_dtype, + use_proxy, + ) for ( pv, tango_type, @@ -467,9 +493,15 @@ async def test_tango_signal_r( initial_value, put_value, ) in ATTRIBUTES_SET + for use_dtype in [True, False] + for use_proxy in [True, False] + ], + ids=[ + f"{x[0]}_{use_dtype}_{use_proxy}" + for x in ATTRIBUTES_SET + for use_dtype in [True, False] for use_proxy in [True, False] ], - ids=[f"{x[0]}_{use_proxy}" for x in ATTRIBUTES_SET for use_proxy in [True, False]], ) async def test_tango_signal_w( echo_device: str, @@ -479,11 +511,13 @@ async def test_tango_signal_w( py_type: Type[T], initial_value: T, put_value: T, + use_dtype: bool, use_proxy: bool, ): await prepare_device(echo_device, pv, initial_value) source = echo_device + "/" + pv proxy = await DeviceProxy(echo_device) if use_proxy else None + py_type = py_type if use_dtype else None timeout = 0.1 signal = tango_signal_w( @@ -514,9 +548,18 @@ async def test_tango_signal_w( # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( - "pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy", + "pv, tango_type, d_format, py_type, initial_value, put_value, use_dtype, use_proxy", [ - (pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy) + ( + pv, + tango_type, + d_format, + py_type, + initial_value, + put_value, + use_dtype, + use_proxy, + ) for ( pv, tango_type, @@ -525,9 +568,15 @@ async def test_tango_signal_w( initial_value, put_value, ) in ATTRIBUTES_SET + for use_dtype in [True, False] + for use_proxy in [True, False] + ], + ids=[ + f"{x[0]}_{use_dtype}_{use_proxy}" + for x in ATTRIBUTES_SET + for use_dtype in [True, False] for use_proxy in [True, False] ], - ids=[f"{x[0]}_{use_proxy}" for x in ATTRIBUTES_SET for use_proxy in [True, False]], ) async def test_tango_signal_rw( echo_device: str, @@ -537,11 +586,13 @@ async def test_tango_signal_rw( py_type: Type[T], initial_value: T, put_value: T, + use_dtype: bool, use_proxy: bool, ): await prepare_device(echo_device, pv, initial_value) source = echo_device + "/" + pv proxy = await DeviceProxy(echo_device) if use_proxy else None + py_type = py_type if use_dtype else None timeout = 0.1 signal = tango_signal_rw( @@ -561,48 +612,10 @@ async def test_tango_signal_rw( assert_close(location["readback"], put_value) -# -------------------------------------------------------------------- -@pytest.mark.asyncio -@pytest.mark.parametrize( - "pv, tango_type, d_format, py_type, initial_value, put_value", - COMMANDS_SET, - ids=[x[0] for x in COMMANDS_SET], -) -async def test_tango_signal_x( - echo_device: str, - pv: str, - tango_type: str, - d_format: AttrDataFormat, - py_type: Type[T], - initial_value: T, - put_value: T, -): - source = echo_device + "/" + pv - timeout = 0.1 - signal = tango_signal_auto( - datatype=py_type, - trl=source, - device_proxy=None, - name="test_signal", - timeout=timeout, - ) - await signal.connect() - assert signal - reading = await signal.read() - assert reading["test_signal"]["value"] is None - - await signal.set(put_value, wait=True, timeout=0.1) - reading = await signal.read() - value = reading["test_signal"]["value"] - if isinstance(value, np.ndarray): - value = value.tolist() - assert_close(value, put_value) - - # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize("use_proxy", [True, False]) -async def test_tango_signal_x_none(tango_test_device: str, use_proxy: bool): +async def test_tango_signal_x(tango_test_device: str, use_proxy: bool): proxy = await DeviceProxy(tango_test_device) if use_proxy else None timeout = 0.1 signal = tango_signal_x( @@ -694,8 +707,6 @@ async def _test_signal(dtype, proxy): # -------------------------------------------------------------------- - - @pytest.mark.asyncio @pytest.mark.parametrize( "pv, tango_type, d_format, py_type, initial_value, put_value, use_dtype, use_proxy", @@ -772,7 +783,7 @@ async def _test_signal(dtype, proxy): # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize("use_proxy", [True, False]) -async def test_tango_signal_auto_cmds_none(tango_test_device: str, use_proxy: bool): +async def test_tango_signal_auto_cmds_void(tango_test_device: str, use_proxy: bool): proxy = await DeviceProxy(tango_test_device) if use_proxy else None signal = tango_signal_auto( datatype=None, @@ -783,3 +794,14 @@ async def test_tango_signal_auto_cmds_none(tango_test_device: str, use_proxy: bo await signal.connect() assert signal await signal.trigger(wait=True) + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_signal_auto_badtrl(tango_test_device: str): + with pytest.raises(RuntimeError) as exc_info: + tango_signal_auto( + datatype=None, + trl=tango_test_device + "/" + "badtrl", + ) + assert f"Cannot find badtrl in {tango_test_device}" in str(exc_info.value) From aaaae49c602e3b2487a1e536e960680210ea742b Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 20 Aug 2024 16:09:56 +0200 Subject: [PATCH 096/141] Fixed base device tests. Made datatype optional for tango signals --- src/ophyd_async/tango/signal/signal.py | 9 ++++++--- tests/tango/test_base_device.py | 6 +++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 94152c488e..01eeec24fe 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -33,7 +33,8 @@ def _make_backend( # -------------------------------------------------------------------- def tango_signal_rw( - datatype: Type[T], + datatype: Optional[Type[T]] = None, + *, read_trl: str, write_trl: Optional[str] = None, device_proxy: Optional[DeviceProxy] = None, @@ -63,7 +64,8 @@ def tango_signal_rw( # -------------------------------------------------------------------- def tango_signal_r( - datatype: Type[T], + datatype: Optional[Type[T]] = None, + *, read_trl: str, device_proxy: Optional[DeviceProxy] = None, timeout: float = DEFAULT_TIMEOUT, @@ -90,7 +92,8 @@ def tango_signal_r( # -------------------------------------------------------------------- def tango_signal_w( - datatype: Type[T], + datatype: Optional[Type[T]] = None, + *, write_trl: str, device_proxy: Optional[DeviceProxy] = None, timeout: float = DEFAULT_TIMEOUT, diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index d26b535963..719545b543 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -176,7 +176,11 @@ def __init__(self, trl: str, name="") -> None: def register_signals(self): for feature in TESTED_FEATURES: with self.add_children_as_readables(): - setattr(self, feature, tango_signal_auto(None, f"{self.trl}/{feature}")) + setattr( + self, + feature, + tango_signal_auto(datatype=None, trl=f"{self.trl}/{feature}"), + ) # -------------------------------------------------------------------- From 30f0de9d7e5dec9612882306f1a10ec0bac8c9b8 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 22 Aug 2024 09:05:12 +0200 Subject: [PATCH 097/141] Forgot to add calls to infer python types in non-auto signals --- src/ophyd_async/tango/signal/signal.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 01eeec24fe..7a996ec6b7 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -58,6 +58,8 @@ def tango_signal_rw( name: The name of the Signal """ + if datatype is None: + datatype = infer_python_type(read_trl) backend = _make_backend(datatype, read_trl, write_trl or read_trl, device_proxy) return SignalRW(backend, timeout=timeout, name=name) @@ -86,6 +88,8 @@ def tango_signal_r( name: The name of the Signal """ + if datatype is None: + datatype = infer_python_type(read_trl) backend = _make_backend(datatype, read_trl, read_trl, device_proxy) return SignalR(backend, timeout=timeout, name=name) @@ -114,6 +118,8 @@ def tango_signal_w( name: The name of the Signal """ + if datatype is None: + datatype = infer_python_type(write_trl) backend = _make_backend(datatype, write_trl, write_trl, device_proxy) return SignalW(backend, timeout=timeout, name=name) From 8b53af407fd4a6af96bc5241e0f86a36b0c35a19 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 22 Aug 2024 09:12:23 +0200 Subject: [PATCH 098/141] minor layout changes --- src/ophyd_async/tango/signal/signal.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 7a996ec6b7..cef69a3e7d 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -22,7 +22,7 @@ ) -def _make_backend( +def make_backend( datatype: Optional[Type[T]], read_trl: str, write_trl: str, @@ -60,7 +60,7 @@ def tango_signal_rw( """ if datatype is None: datatype = infer_python_type(read_trl) - backend = _make_backend(datatype, read_trl, write_trl or read_trl, device_proxy) + backend = make_backend(datatype, read_trl, write_trl or read_trl, device_proxy) return SignalRW(backend, timeout=timeout, name=name) @@ -90,7 +90,7 @@ def tango_signal_r( """ if datatype is None: datatype = infer_python_type(read_trl) - backend = _make_backend(datatype, read_trl, read_trl, device_proxy) + backend = make_backend(datatype, read_trl, read_trl, device_proxy) return SignalR(backend, timeout=timeout, name=name) @@ -120,7 +120,7 @@ def tango_signal_w( """ if datatype is None: datatype = infer_python_type(write_trl) - backend = _make_backend(datatype, write_trl, write_trl, device_proxy) + backend = make_backend(datatype, write_trl, write_trl, device_proxy) return SignalW(backend, timeout=timeout, name=name) @@ -144,7 +144,7 @@ def tango_signal_x( name: The name of the Signal """ - backend = _make_backend(None, write_trl, write_trl, device_proxy) + backend = make_backend(None, write_trl, write_trl, device_proxy) return SignalX(backend, timeout=timeout, name=name) @@ -157,13 +157,12 @@ def tango_signal_auto( timeout: float = DEFAULT_TIMEOUT, name: str = "", ) -> Union[SignalW, SignalX, SignalR, SignalRW]: - device_trl, tr_name = trl.rsplit("/", 1) - syn_proxy = SyncDeviceProxy(device_trl) - if datatype is None: datatype = infer_python_type(trl) - backend = _make_backend(datatype, trl, trl, device_proxy) + device_trl, tr_name = trl.rsplit("/", 1) + syn_proxy = SyncDeviceProxy(device_trl) + backend = make_backend(datatype, trl, trl, device_proxy) if tr_name not in syn_proxy.get_attribute_list(): if tr_name not in syn_proxy.get_command_list(): From b6283909c6bf7e410faeb7804f1e9446c75e7154 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 22 Aug 2024 09:36:36 +0200 Subject: [PATCH 099/141] make_backend and infer_python_type added to init --- src/ophyd_async/tango/signal/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ophyd_async/tango/signal/__init__.py b/src/ophyd_async/tango/signal/__init__.py index e006b3645c..bc84f0e250 100644 --- a/src/ophyd_async/tango/signal/__init__.py +++ b/src/ophyd_async/tango/signal/__init__.py @@ -1,4 +1,6 @@ from .signal import ( + infer_python_type, + make_backend, tango_signal_auto, tango_signal_r, tango_signal_rw, @@ -12,4 +14,6 @@ "tango_signal_w", "tango_signal_x", "tango_signal_auto", + "make_backend", + "infer_python_type", ) From 6f87509213ca3bbf3d24c182049c6181c0475ef5 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 22 Aug 2024 10:05:04 +0200 Subject: [PATCH 100/141] Moved inference of signal frontend to its own method so that its callable from the base device --- src/ophyd_async/tango/signal/signal.py | 59 ++++++++++++++------------ 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index cef69a3e7d..5ce9c9eda5 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -159,37 +159,15 @@ def tango_signal_auto( ) -> Union[SignalW, SignalX, SignalR, SignalRW]: if datatype is None: datatype = infer_python_type(trl) - - device_trl, tr_name = trl.rsplit("/", 1) - syn_proxy = SyncDeviceProxy(device_trl) backend = make_backend(datatype, trl, trl, device_proxy) + signal = infer_signal_frontend(trl, name, timeout) + signal._backend = backend # noqa: SLF001 - if tr_name not in syn_proxy.get_attribute_list(): - if tr_name not in syn_proxy.get_command_list(): - raise RuntimeError(f"Cannot find {tr_name} in {device_trl}") - - if tr_name in syn_proxy.get_attribute_list(): - config = syn_proxy.get_attribute_config(tr_name) - if config.writable in [AttrWriteType.READ_WRITE, AttrWriteType.READ_WITH_WRITE]: - return SignalRW(backend, timeout=timeout, name=name) - elif config.writable == AttrWriteType.READ: - return SignalR(backend, timeout=timeout, name=name) - else: - return SignalW(backend, timeout=timeout, name=name) - - if tr_name in syn_proxy.get_command_list(): - config = syn_proxy.get_command_config(tr_name) - if config.in_type == CmdArgType.DevVoid: - return SignalX(backend, timeout=timeout, name=name) - elif config.out_type != CmdArgType.DevVoid: - return SignalRW(backend, timeout=timeout, name=name) - - if tr_name in device_proxy.get_pipe_list(): - raise NotImplementedError("Pipes are not supported") + return signal # -------------------------------------------------------------------- -def infer_python_type(trl: str): +def infer_python_type(trl: str) -> Type: device_trl, tr_name = trl.rsplit("/", 1) syn_proxy = SyncDeviceProxy(device_trl) @@ -211,3 +189,32 @@ def infer_python_type(trl: str): py_type = DevState return npt.NDArray[py_type] if isarray else py_type + + +# -------------------------------------------------------------------- +def infer_signal_frontend(trl, name: str = "", timeout: float = DEFAULT_TIMEOUT): + device_trl, tr_name = trl.rsplit("/", 1) + proxy = SyncDeviceProxy(device_trl) + + if tr_name in proxy.get_pipe_list(): + raise NotImplementedError("Pipes are not supported") + + if tr_name not in proxy.get_attribute_list(): + if tr_name not in proxy.get_command_list(): + raise RuntimeError(f"Cannot find {tr_name} in {device_trl}") + + if tr_name in proxy.get_attribute_list(): + config = proxy.get_attribute_config(tr_name) + if config.writable in [AttrWriteType.READ_WRITE, AttrWriteType.READ_WITH_WRITE]: + return SignalRW(name=name, timeout=timeout) + elif config.writable == AttrWriteType.READ: + return SignalR(name=name, timeout=timeout) + else: + return SignalW(name=name, timeout=timeout) + + if tr_name in proxy.get_command_list(): + config = proxy.get_command_config(tr_name) + if config.in_type == CmdArgType.DevVoid: + return SignalX(name=name, timeout=timeout) + elif config.out_type != CmdArgType.DevVoid: + return SignalRW(name=name, timeout=timeout) From b2b9cc7d5ad783c022f125bbbf0e64dfcd5b8cc4 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 22 Aug 2024 10:06:48 +0200 Subject: [PATCH 101/141] minor typehint change --- src/ophyd_async/tango/signal/signal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 5ce9c9eda5..bf137e8bd4 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -167,7 +167,7 @@ def tango_signal_auto( # -------------------------------------------------------------------- -def infer_python_type(trl: str) -> Type: +def infer_python_type(trl: str) -> Type[T]: device_trl, tr_name = trl.rsplit("/", 1) syn_proxy = SyncDeviceProxy(device_trl) From 34c5143ce727db89712a52cf078bcb948929b0eb Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 22 Aug 2024 10:11:29 +0200 Subject: [PATCH 102/141] Updated imports in init --- src/ophyd_async/tango/signal/__init__.py | 2 ++ src/ophyd_async/tango/signal/signal.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/ophyd_async/tango/signal/__init__.py b/src/ophyd_async/tango/signal/__init__.py index bc84f0e250..6f5804db5d 100644 --- a/src/ophyd_async/tango/signal/__init__.py +++ b/src/ophyd_async/tango/signal/__init__.py @@ -1,5 +1,6 @@ from .signal import ( infer_python_type, + infer_signal_frontend, make_backend, tango_signal_auto, tango_signal_r, @@ -16,4 +17,5 @@ "tango_signal_auto", "make_backend", "infer_python_type", + "infer_signal_frontend", ) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index bf137e8bd4..e4fa4f907f 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -19,6 +19,9 @@ "tango_signal_w", "tango_signal_x", "tango_signal_auto", + "infer_signal_frontend", + "infer_python_type", + "make_backend", ) From 6330c5bffb32c73de1aefcb7c52c176a49f727bd Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 22 Aug 2024 15:54:34 +0200 Subject: [PATCH 103/141] Demo for tango test devices --- .../tango/_backend/_tango_transport.py | 36 ++++-- .../tango/base_devices/base_device.py | 72 +++++++++-- src/ophyd_async/tango/demo/README.md | 56 ++++++++ src/ophyd_async/tango/demo/__init__.py | 12 ++ src/ophyd_async/tango/demo/_tango/__init__.py | 3 + src/ophyd_async/tango/demo/_tango/servers.py | 120 ++++++++++++++++++ src/ophyd_async/tango/demo/counter.py | 52 ++++++++ src/ophyd_async/tango/demo/mover.py | 111 ++++++++++++++++ tests/tango/test_base_device.py | 16 ++- tests/tango/test_tango_transport.py | 21 +-- 10 files changed, 461 insertions(+), 38 deletions(-) create mode 100644 src/ophyd_async/tango/demo/README.md create mode 100644 src/ophyd_async/tango/demo/__init__.py create mode 100644 src/ophyd_async/tango/demo/_tango/__init__.py create mode 100644 src/ophyd_async/tango/demo/_tango/servers.py create mode 100644 src/ophyd_async/tango/demo/counter.py create mode 100644 src/ophyd_async/tango/demo/mover.py diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 6408bfb0bc..925303c985 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -82,7 +82,7 @@ def get_python_type(tango_type) -> tuple[bool, T, str]: # -------------------------------------------------------------------- class TangoProxy: - support_events = False + support_events = True def __init__(self, device_proxy: DeviceProxy, name: str): self._proxy = device_proxy @@ -149,13 +149,15 @@ def set_polling( # -------------------------------------------------------------------- class AttributeProxy(TangoProxy): _callback = None - support_events = False + support_events = True _eid = None _poll_task = None _abs_change = None _rel_change = 0.1 - _polling_period = 0.5 + _polling_period = 0.1 _allow_polling = False + exception = None + _last_reading = {"value": None, "timestamp": 0, "alarm_severity": 0} # -------------------------------------------------------------------- async def connect(self) -> None: @@ -233,6 +235,7 @@ async def get_reading(self) -> Reading: reading = Reading( value=attr.value, timestamp=attr.time.totime(), alarm_severity=attr.quality ) + self._last_reading = reading return reading # -------------------------------------------------------------------- @@ -259,7 +262,16 @@ def subscribe_callback(self, callback: Optional[ReadingValueCallback]): elif self._allow_polling: """start polling if no events supported""" if self._callback is not None: - self._poll_task = asyncio.create_task(self.poll()) + + async def _poll(): + while True: + try: + await self.poll() + except RuntimeError as e: + self.exception = f"Error in polling: {e}" + await asyncio.sleep(1) + + self._poll_task = asyncio.create_task(_poll()) else: self.unsubscribe_callback() raise RuntimeError( @@ -276,6 +288,12 @@ def unsubscribe_callback(self): if self._poll_task: self._poll_task.cancel() self._poll_task = None + if self._callback is not None: + # Call the callback with the last reading + try: + self._callback(self._last_reading, self._last_reading["value"]) + except TypeError: + pass self._callback = None # -------------------------------------------------------------------- @@ -381,7 +399,7 @@ def set_polling( # -------------------------------------------------------------------- class CommandProxy(TangoProxy): - support_events = False + support_events = True _last_reading = {"value": None, "timestamp": 0, "alarm_severity": 0} # -------------------------------------------------------------------- @@ -615,8 +633,8 @@ def __init__( } self.trl_configs: Dict[str, AttributeInfoEx] = {} self.descriptor: Descriptor = {} # type: ignore - self.polling = (True, 0.5, None, 0.1) - self.support_events = False + self.polling = (False, 0.1, None, 0.1) + self.support_events = True self.status = None # -------------------------------------------------------------------- @@ -699,8 +717,8 @@ def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: def set_polling( self, - allow_polling: bool = False, - polling_period: float = 0.5, + allow_polling: bool = True, + polling_period: float = 0.1, abs_change=None, rel_change=0.1, ): diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index 8a6d539c2c..724cc52289 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -1,8 +1,22 @@ from __future__ import annotations +import asyncio from typing import Optional -from ophyd_async.core import DEFAULT_TIMEOUT, StandardReadable +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + Signal, + SignalR, + SignalRW, + SignalW, + SignalX, + StandardReadable, +) +from ophyd_async.tango.signal import ( + infer_python_type, + infer_signal_frontend, + make_backend, +) from tango.asyncio import DeviceProxy __all__ = ("TangoReadableDevice",) @@ -33,6 +47,7 @@ class TangoReadableDevice(StandardReadable): def __init__(self, trl: str, name="") -> None: self.trl = trl self.proxy: Optional[DeviceProxy] = None + self.create_children_from_annotations() StandardReadable.__init__(self, name=name) async def connect( @@ -42,15 +57,56 @@ async def connect( force_reconnect: bool = False, ): async def closure(): - self.proxy = await DeviceProxy(self.trl) + if self.proxy is None: + self.proxy = await DeviceProxy(self.trl) + elif isinstance(self.proxy, asyncio.Future): + self.proxy = await self.proxy return self await closure() - - # If a register_signals method is defined, call it - if hasattr(self, "register_signals"): - self.register_signals() - # set_name must be called again to propagate the new signal names - self.set_name(self.name) + self.register_signals() + # set_name should be called again to propagate the new signal names + self.set_name(self.name) await super().connect(mock=mock, timeout=timeout) + + def register_signals(self): + for name, obj_type in self.__annotations__.items(): + if hasattr(self, name): + signal = getattr(self, name) + if issubclass(type(signal), Signal): + tango_name = name.lstrip("_") + read_trl = f"{self.trl}/{tango_name}" + datatype = infer_python_type(read_trl) + backend = make_backend( + datatype=datatype, + read_trl=read_trl, + write_trl=read_trl, + device_proxy=self.proxy, + ) + signal._backend = backend # noqa: SLF001 + + def create_children_from_annotations(self): + for attr_name, obj_type in self.__annotations__.items(): + if ( + isinstance(obj_type, type) + and issubclass(obj_type, Signal) + or obj_type is None + ): + if obj_type is SignalRW: + setattr(self, attr_name, SignalRW()) + elif obj_type is SignalR: + setattr(self, attr_name, SignalR()) + elif obj_type is SignalW: + setattr(self, attr_name, SignalW()) + elif obj_type is SignalX: + setattr(self, attr_name, SignalX()) + elif obj_type is Signal or None: + tango_name = attr_name.lstrip("_") + setattr( + self, + attr_name, + infer_signal_frontend(trl=f"{self.trl}/" f"{tango_name}"), + ) + else: + raise ValueError(f"Invalid signal type {obj_type}") diff --git a/src/ophyd_async/tango/demo/README.md b/src/ophyd_async/tango/demo/README.md new file mode 100644 index 0000000000..d41c160f15 --- /dev/null +++ b/src/ophyd_async/tango/demo/README.md @@ -0,0 +1,56 @@ +The following is intended to be a demonstration of Tango support for ophyd-async. +The usage of the Tango control system without a real Tango server is limited and +intended to be used in tests. All operations using the demo devices must be performed +within the MultiDeviceTestContext context as demonstrated here. + +```python +from tango.test_context import MultiDeviceTestContext +from ophyd_async.tango.demo import ( + DemoMover, + TangoMover, + DemoCounter, + TangoCounter, +) + +from bluesky import RunEngine +import bluesky.plans as bp +from bluesky.callbacks.best_effort import BestEffortCallback +from bluesky.utils import ProgressBarManager + +content = ( + { + "class": DemoMover, + "devices": [ + {"name": "demo/motor/1"} + ], + }, + { + "class": DemoCounter, + "devices": [ + {"name": "demo/counter/1"}, + {"name": "demo/counter/2"} + ], + } +) +tango_context = MultiDeviceTestContext(content) +with tango_context as context: + motor1 = TangoMover(trl=context.get_device_access("demo/motor/1"), name="motor1") + counter1 = TangoCounter(trl=context.get_device_access("demo/counter/1"), name="counter1") + counter2 = TangoCounter(trl=context.get_device_access("demo/counter/2"), name="counter2") + await motor1.connect() + await counter1.connect() + await counter2.connect() + + # Events are not supported by the test context so we disable them + motor1.position._backend.allow_events(False) + motor1.state._backend.allow_events(False) + # Enable polling for the position and state attributes + motor1.position._backend.set_polling(True, 0.1, 0.1) + motor1.state._backend.set_polling(True, 0.1) + + RE = RunEngine() + RE.subscribe(BestEffortCallback()) + RE.waiting_hook = ProgressBarManager() + #RE(bps.mv(motor1, 1)) + RE(bp.scan([counter1, counter2], motor1, -1, 1, 10)) +``` \ No newline at end of file diff --git a/src/ophyd_async/tango/demo/__init__.py b/src/ophyd_async/tango/demo/__init__.py new file mode 100644 index 0000000000..4c9cb10a23 --- /dev/null +++ b/src/ophyd_async/tango/demo/__init__.py @@ -0,0 +1,12 @@ +from ._tango import DemoCounter, DemoMover +from .counter import TangoCounter, TangoCounterConfig +from .mover import TangoMover, TangoMoverConfig + +__all__ = [ + "DemoCounter", + "DemoMover", + "TangoCounter", + "TangoCounterConfig", + "TangoMover", + "TangoMoverConfig", +] diff --git a/src/ophyd_async/tango/demo/_tango/__init__.py b/src/ophyd_async/tango/demo/_tango/__init__.py new file mode 100644 index 0000000000..10881b4e77 --- /dev/null +++ b/src/ophyd_async/tango/demo/_tango/__init__.py @@ -0,0 +1,3 @@ +from .servers import DemoCounter, DemoMover + +__all__ = ["DemoCounter", "DemoMover"] diff --git a/src/ophyd_async/tango/demo/_tango/servers.py b/src/ophyd_async/tango/demo/_tango/servers.py new file mode 100644 index 0000000000..f253228908 --- /dev/null +++ b/src/ophyd_async/tango/demo/_tango/servers.py @@ -0,0 +1,120 @@ +import asyncio +import time + +import numpy as np + +from tango import AttrWriteType, DevState, GreenMode +from tango.server import Device, attribute, command + + +class DemoMover(Device): + green_mode = GreenMode.Asyncio + _position = 0.0 + _setpoint = 0.0 + _velocity = 0.5 + _acceleration = 0.5 + _precision = 0.1 + _stop = False + DEVICE_CLASS_INITIAL_STATE = DevState.ON + + @attribute(dtype=float, access=AttrWriteType.READ_WRITE) + async def position(self): + return self._position + + async def write_position(self, new_position): + self._setpoint = new_position + await self.move() + + @attribute(dtype=float, access=AttrWriteType.READ_WRITE) + async def setpoint(self): + return self._setpoint + + async def write_setpoint(self, new_position): + self._setpoint = new_position + + @attribute(dtype=float, access=AttrWriteType.READ_WRITE) + async def velocity(self): + return self._velocity + + async def write_velocity(self, value: float): + self._velocity = value + + async def write_precision(self, value: float): + self._precision = value + + @attribute(dtype=DevState, access=AttrWriteType.READ) + async def state(self): + return self.get_state() + + @command + async def stop(self): + self._stop = True + + @command + async def move(self): + self.set_state(DevState.MOVING) + await self._move(self._setpoint) + self.set_state(DevState.ON) + + async def _move(self, new_position): + self._setpoint = new_position + self._stop = False + step = 0.1 + while True: + if self._stop: + self._stop = False + break + if self._position < new_position: + self._position = self._position + self._velocity * step + else: + self._position = self._position - self._velocity * step + if abs(self._position - new_position) < self._precision: + self._position = new_position + break + await asyncio.sleep(step) + + +class DemoCounter(Device): + green_mode = GreenMode.Asyncio + _counts = 0 + _sample_time = 1.0 + + @attribute(dtype=int, access=AttrWriteType.READ) + async def counts(self): + return self._counts + + @attribute(dtype=float, access=AttrWriteType.READ_WRITE) + async def sample_time(self): + return self._sample_time + + async def write_sample_time(self, value: float): + if value < 0.0: + raise ValueError("Sample time must be a positive number") + self._sample_time = value + + @attribute(dtype=DevState, access=AttrWriteType.READ) + async def state(self): + return self.get_state() + + @command + async def reset(self): + self._counts = 0 + return self._counts + + @command + async def start(self): + self._counts = 0 + if self._sample_time <= 0.0: + return + self.set_state(DevState.MOVING) + await self._trigger() + self.set_state(DevState.ON) + + async def _trigger(self): + st = time.time() + while True: + ct = time.time() + if ct - st > self._sample_time: + break + self._counts += int(np.random.normal(1000, 100)) + await asyncio.sleep(0.1) diff --git a/src/ophyd_async/tango/demo/counter.py b/src/ophyd_async/tango/demo/counter.py new file mode 100644 index 0000000000..d8c6df28b9 --- /dev/null +++ b/src/ophyd_async/tango/demo/counter.py @@ -0,0 +1,52 @@ +from dataclasses import dataclass +from typing import Optional + +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + AsyncStatus, + ConfigSignal, + HintedSignal, + Signal, + SignalX, +) +from ophyd_async.tango import ( + TangoReadableDevice, +) + + +@dataclass +class TangoCounterConfig: + sample_time: Optional[float] = None + + +class TangoCounter(TangoReadableDevice): + # Enter the name and type of the signals you want to use + # If type is None or Signal, the type will be inferred from the Tango device + counts: None + sample_time: Signal + state: Signal + reset: Signal + start: SignalX + + def __init__(self, trl: str, name=""): + super().__init__(trl, name=name) + self.add_readables([self.counts], HintedSignal.uncached) + self.add_readables([self.sample_time], ConfigSignal) + + def trigger(self): + return AsyncStatus(self._trigger()) + + async def _trigger(self): + sample_time = await self.sample_time.get_value() + timeout = sample_time + DEFAULT_TIMEOUT + await self.start.trigger(wait=True, timeout=timeout) + + def prepare(self, value: TangoCounterConfig) -> AsyncStatus: + return AsyncStatus(self._prepare(value)) + + async def _prepare(self, value: TangoCounterConfig) -> None: + config = value.__dataclass_fields__ + for key, v in config.items(): + if v is not None: + if hasattr(self, key): + await getattr(self, key).set(v) diff --git a/src/ophyd_async/tango/demo/mover.py b/src/ophyd_async/tango/demo/mover.py new file mode 100644 index 0000000000..cc4eb7ee35 --- /dev/null +++ b/src/ophyd_async/tango/demo/mover.py @@ -0,0 +1,111 @@ +import asyncio +from dataclasses import dataclass +from typing import Optional + +from bluesky.protocols import Movable, Reading, Stoppable + +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + AsyncStatus, + CalculatableTimeout, + CalculateTimeout, + ConfigSignal, + HintedSignal, + SignalR, + SignalRW, + SignalX, + WatchableAsyncStatus, + WatcherUpdate, + observe_value, +) +from ophyd_async.tango import ( + TangoReadableDevice, +) +from tango import ( + DevState, +) + + +@dataclass +class TangoMoverConfig: + velocity: Optional[float] = None + + +class TangoMover(TangoReadableDevice, Movable, Stoppable): + # Enter the name and type of the signals you want to use + # If type is None or Signal, the type will be inferred from the Tango device + position: SignalRW + velocity: SignalRW + state: SignalR + _stop: SignalX + + def __init__(self, trl: str, name=""): + super().__init__(trl, name=name) + self.add_readables([self.position], HintedSignal) + self.add_readables([self.velocity], ConfigSignal) + self._set_success = True + + def set(self, value: float, timeout: CalculatableTimeout = CalculateTimeout): + return WatchableAsyncStatus(self._set(value, timeout)) + + async def _set(self, value: float, timeout: CalculatableTimeout = CalculateTimeout): + self._set_success = True + (old_position, velocity) = await asyncio.gather( + self.position.get_value(), self.velocity.get_value() + ) + if timeout is CalculateTimeout: + assert velocity > 0, "Motor has zero velocity" + timeout = abs(value - old_position) / velocity + DEFAULT_TIMEOUT + + # For this server, set returns immediately so this status should not be awaited + await self.position.set(value, wait=False, timeout=timeout) + + # Wait for the motor to stop + move_status = AsyncStatus(self._wait_for_idle()) + + try: + async for current_position in observe_value( + self.position, done_status=move_status + ): + yield WatcherUpdate( + current=current_position, + initial=old_position, + target=value, + name=self.name, + ) + except RuntimeError as exc: + self._set_success = False + raise RuntimeError("Motor was stopped") from exc + if not self._set_success: + raise RuntimeError("Motor was stopped") + + async def _wait_for_idle(self): + if self.state._backend.support_events is False: # noqa: SLF001 + if self.state._backend.polling[0] is False: # noqa: SLF001 + raise RuntimeError("State does not support events or polling") + + event = asyncio.Event() + + def _wait(value: dict[str, Reading]): + if value[self.state.name]["value"] == DevState.ON: + event.set() + + self.state.subscribe(_wait) + await event.wait() + + def stop(self, success: bool = True) -> AsyncStatus: + self._set_success = success + return self._stop.trigger() + + def prepare(self, value: TangoMoverConfig) -> AsyncStatus: + return AsyncStatus(self._prepare(value)) + + async def _prepare(self, value: TangoMoverConfig) -> None: + config = value.__dataclass_fields__ + for key, v in config.items(): + if v is not None: + if hasattr(self, key): + await getattr(self, key).set(v) + + async def get_dataclass(self) -> TangoMoverConfig: + return TangoMoverConfig() diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 719545b543..65571dbda8 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -175,12 +175,13 @@ def __init__(self, trl: str, name="") -> None: def register_signals(self): for feature in TESTED_FEATURES: - with self.add_children_as_readables(): - setattr( - self, - feature, - tango_signal_auto(datatype=None, trl=f"{self.trl}/{feature}"), - ) + setattr( + self, + feature, + tango_signal_auto(datatype=None, trl=f"{self.trl}/{feature}"), + ) + attr = getattr(self, feature) + self.add_readables([attr]) # -------------------------------------------------------------------- @@ -309,4 +310,7 @@ async def connect(): # now let's do some bluesky stuff RE = RunEngine() + for readable in ophyd_dev._readables: + readable._backend.allow_events(False) + readable._backend.set_polling(True, 0.1, 0.1) RE(count([ophyd_dev], 1)) diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index 77439ad607..6e8d0d538d 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -337,7 +337,6 @@ async def test_attribute_subscribe_callback(echo_device): val = None def callback(reading, value): - print("Callback called") nonlocal val val = value @@ -389,6 +388,7 @@ def test_attribute_set_polling(device_proxy): @pytest.mark.asyncio async def test_attribute_poll(device_proxy): attr_proxy = AttributeProxy(device_proxy, "floatvalue") + attr_proxy.support_events = False def callback(reading, value): nonlocal val @@ -399,6 +399,7 @@ def bad_callback(): # Test polling with absolute change val = None + attr_proxy.set_polling(True, 0.1, 1, 1.0) attr_proxy.subscribe_callback(callback) current_value = await attr_proxy.get() @@ -437,7 +438,7 @@ def bad_callback(): # Test polling with bad callback attr_proxy.subscribe_callback(bad_callback) await asyncio.sleep(0.2) - assert "Could not poll the attribute" in str(attr_proxy._poll_task.exception()) + assert "Could not poll the attribute" in str(attr_proxy.exception) attr_proxy.unsubscribe_callback() @@ -446,6 +447,7 @@ def bad_callback(): @pytest.mark.parametrize("attr", ["array", "label"]) async def test_attribute_poll_stringsandarrays(device_proxy, attr): attr_proxy = AttributeProxy(device_proxy, attr) + attr_proxy.support_events = False def callback(reading, value): nonlocal val @@ -471,6 +473,7 @@ def callback(reading, value): async def test_attribute_poll_exceptions(device_proxy): # Try to poll a non-existent attribute attr_proxy = AttributeProxy(device_proxy, "nonexistent") + attr_proxy.support_events = False attr_proxy.set_polling(True, 0.1) def callback(reading, value): @@ -478,18 +481,7 @@ def callback(reading, value): attr_proxy.subscribe_callback(callback) await asyncio.sleep(0.2) - assert "Could not poll the attribute" in str(attr_proxy._poll_task.exception()) - - # Try to poll a command - attr_proxy = AttributeProxy(device_proxy, "clear") - attr_proxy.set_polling(True, 0.1) - - def callback(reading, value): - pass - - attr_proxy.subscribe_callback(callback) - await asyncio.sleep(0.2) - assert "Could not poll the attribute" in str(attr_proxy._poll_task.exception()) + assert "Could not poll the attribute" in str(attr_proxy.exception) # -------------------------------------------------------------------- @@ -696,7 +688,6 @@ def callback(reading, value): transport.set_callback(None) with pytest.raises(RuntimeError) as exc_info: transport.set_callback(1) - print(exc_info.value) assert "Callback must be a callable" in str(exc_info.value) From 7c7372b7a7b5297d454de774220babd7ebeec056 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 22 Aug 2024 16:32:05 +0200 Subject: [PATCH 104/141] Added demo test --- .../tango/base_devices/base_device.py | 3 +- src/ophyd_async/tango/signal/signal.py | 3 -- tests/tango/test_base_device.py | 54 +++++++++++++++++-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index 724cc52289..71e8108077 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -101,7 +101,7 @@ def create_children_from_annotations(self): setattr(self, attr_name, SignalW()) elif obj_type is SignalX: setattr(self, attr_name, SignalX()) - elif obj_type is Signal or None: + elif obj_type is Signal or obj_type is None: tango_name = attr_name.lstrip("_") setattr( self, @@ -109,4 +109,5 @@ def create_children_from_annotations(self): infer_signal_frontend(trl=f"{self.trl}/" f"{tango_name}"), ) else: + print(obj_type, type(obj_type)) raise ValueError(f"Invalid signal type {obj_type}") diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index e4fa4f907f..bf137e8bd4 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -19,9 +19,6 @@ "tango_signal_w", "tango_signal_x", "tango_signal_auto", - "infer_signal_frontend", - "infer_python_type", - "make_backend", ) diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 65571dbda8..6acb156583 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -2,20 +2,23 @@ from enum import Enum, IntEnum from typing import Type +import bluesky.plan_stubs as bps +import bluesky.plans as bp import numpy as np import pytest from bluesky import RunEngine -from bluesky.plans import count from ophyd_async.core import DeviceCollector, T from ophyd_async.tango import TangoReadableDevice, tango_signal_auto from ophyd_async.tango._backend._tango_transport import get_python_type +from ophyd_async.tango.demo._tango.servers import DemoCounter, DemoMover +from ophyd_async.tango.demo.counter import TangoCounter +from ophyd_async.tango.demo.mover import TangoMover from tango import ( AttrDataFormat, AttrQuality, AttrWriteType, CmdArgType, - # DeviceProxy, DevState, ) from tango.asyncio import DeviceProxy @@ -271,6 +274,22 @@ def tango_test_device(): yield context.get_device_access("test/device/1") +# -------------------------------------------------------------------- +@pytest.fixture(scope="module") +def demo_test_context(): + content = ( + { + "class": DemoMover, + "devices": [{"name": "demo/motor/1"}], + }, + { + "class": DemoCounter, + "devices": [{"name": "demo/counter/1"}, {"name": "demo/counter/2"}], + }, + ) + yield MultiDeviceTestContext(content) + + # -------------------------------------------------------------------- @pytest.fixture(autouse=True) def reset_tango_asyncio(): @@ -313,4 +332,33 @@ async def connect(): for readable in ophyd_dev._readables: readable._backend.allow_events(False) readable._backend.set_polling(True, 0.1, 0.1) - RE(count([ophyd_dev], 1)) + RE(bp.count([ophyd_dev], 1)) + + +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_demo(demo_test_context): + with demo_test_context: + motor1 = TangoMover( + trl=demo_test_context.get_device_access("demo/motor/1"), name="motor1" + ) + counter1 = TangoCounter( + trl=demo_test_context.get_device_access("demo/counter/1"), name="counter1" + ) + counter2 = TangoCounter( + trl=demo_test_context.get_device_access("demo/counter/2"), name="counter2" + ) + await motor1.connect() + await counter1.connect() + await counter2.connect() + + # Events are not supported by the test context so we disable them + motor1.position._backend.allow_events(False) + motor1.state._backend.allow_events(False) + # Enable polling for the position and state attributes + motor1.position._backend.set_polling(True, 0.1, 0.1) + motor1.state._backend.set_polling(True, 0.1) + + RE = RunEngine() + RE(bps.read(motor1.position)) + RE(bp.count([counter1, counter2])) From da1971cbea9ee9c7dfc798d23ca364c65a99c45f Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 26 Aug 2024 08:59:03 +0200 Subject: [PATCH 105/141] Renamed TangoTransport to TangoSignalBackend for codebase consistency. --- src/ophyd_async/tango/_backend/__init__.py | 4 ++-- src/ophyd_async/tango/_backend/_tango_transport.py | 4 ++-- src/ophyd_async/tango/signal/signal.py | 9 ++++++--- tests/tango/test_tango_signals.py | 6 +++--- tests/tango/test_tango_transport.py | 12 ++++++------ 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/ophyd_async/tango/_backend/__init__.py b/src/ophyd_async/tango/_backend/__init__.py index 81b66e539d..96091f90c7 100644 --- a/src/ophyd_async/tango/_backend/__init__.py +++ b/src/ophyd_async/tango/_backend/__init__.py @@ -1,3 +1,3 @@ -from ophyd_async.tango._backend._tango_transport import TangoTransport +from ophyd_async.tango._backend._tango_transport import TangoSignalBackend -__all__ = ("TangoTransport",) +__all__ = ("TangoSignalBackend",) diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_transport.py index 925303c985..78a4f1820a 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_transport.py @@ -37,7 +37,7 @@ ) from tango.utils import is_array, is_binary, is_bool, is_float, is_int, is_str -__all__ = ["TangoTransport"] +__all__ = ["TangoSignalBackend"] # time constant to wait for timeout @@ -616,7 +616,7 @@ async def get_tango_trl( # -------------------------------------------------------------------- -class TangoTransport(SignalBackend[T]): +class TangoSignalBackend(SignalBackend[T]): def __init__( self, datatype: Optional[Type[T]], diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index bf137e8bd4..90a87fe936 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -8,7 +8,10 @@ import numpy.typing as npt from ophyd_async.core import DEFAULT_TIMEOUT, SignalR, SignalRW, SignalW, SignalX, T -from ophyd_async.tango._backend._tango_transport import TangoTransport, get_python_type +from ophyd_async.tango._backend._tango_transport import ( + TangoSignalBackend, + get_python_type, +) from tango import AttrDataFormat, AttrWriteType, CmdArgType, DevState from tango import DeviceProxy as SyncDeviceProxy from tango.asyncio import DeviceProxy @@ -27,8 +30,8 @@ def make_backend( read_trl: str, write_trl: str, device_proxy: Optional[DeviceProxy] = None, -) -> TangoTransport: - return TangoTransport(datatype, read_trl, write_trl, device_proxy) +) -> TangoSignalBackend: + return TangoSignalBackend(datatype, read_trl, write_trl, device_proxy) # -------------------------------------------------------------------- diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index 3d5eb0e19b..da614c7d9b 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -12,7 +12,7 @@ from test_base_device import TestDevice from ophyd_async.core import SignalBackend, SignalRW, SignalX, T -from ophyd_async.tango._backend import TangoTransport +from ophyd_async.tango._backend import TangoSignalBackend from ophyd_async.tango.signal import ( tango_signal_auto, tango_signal_r, @@ -243,8 +243,8 @@ async def make_backend( pv: str, connect: bool = True, allow_events: Optional[bool] = True, -) -> TangoTransport: - backend = TangoTransport(typ, pv, pv) +) -> TangoSignalBackend: + backend = TangoSignalBackend(typ, pv, pv) backend.allow_events(allow_events) if connect: await asyncio.wait_for(backend.connect(), 10) diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index 6e8d0d538d..79720290b5 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -14,7 +14,7 @@ from ophyd_async.tango._backend._tango_transport import ( AttributeProxy, CommandProxy, - TangoTransport, + TangoSignalBackend, ensure_proper_executor, get_dtype_extended, get_python_type, @@ -719,7 +719,7 @@ async def test_tango_transport_read_and_write_trl(device_proxy): write_trl = trl + "/" + "setpoint" # Test with existing proxy - transport = TangoTransport(float, read_trl, write_trl, device_proxy) + transport = TangoSignalBackend(float, read_trl, write_trl, device_proxy) await transport.connect() reading = await transport.get_reading() initial_value = reading["value"] @@ -729,7 +729,7 @@ async def test_tango_transport_read_and_write_trl(device_proxy): assert updated_value == new_value # Without pre-existing proxy - transport = TangoTransport(float, read_trl, write_trl, None) + transport = TangoSignalBackend(float, read_trl, write_trl, None) await transport.connect() reading = await transport.get_reading() initial_value = reading["value"] @@ -746,7 +746,7 @@ async def test_tango_transport_read_only_trl(device_proxy): read_trl = trl + "/" + "readonly" # Test with existing proxy - transport = TangoTransport(int, read_trl, read_trl, device_proxy) + transport = TangoSignalBackend(int, read_trl, read_trl, device_proxy) await transport.connect() with pytest.raises(RuntimeError) as exc_info: await transport.put(1) @@ -760,13 +760,13 @@ async def test_tango_transport_nonexistent_trl(device_proxy): nonexistent_trl = trl + "/" + "nonexistent" # Test with existing proxy - transport = TangoTransport(int, nonexistent_trl, nonexistent_trl, device_proxy) + transport = TangoSignalBackend(int, nonexistent_trl, nonexistent_trl, device_proxy) with pytest.raises(RuntimeError) as exc_info: await transport.connect() assert "cannot be found" in str(exc_info.value) # Without pre-existing proxy - transport = TangoTransport(int, nonexistent_trl, nonexistent_trl, None) + transport = TangoSignalBackend(int, nonexistent_trl, nonexistent_trl, None) with pytest.raises(RuntimeError) as exc_info: await transport.connect() assert "cannot be found" in str(exc_info.value) From eb1fd54652c9e8b85d67f83043985c1b095056ca Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 26 Aug 2024 09:01:17 +0200 Subject: [PATCH 106/141] Wrapped trigger in status decorator to more cleanly return a status object. --- src/ophyd_async/tango/demo/counter.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ophyd_async/tango/demo/counter.py b/src/ophyd_async/tango/demo/counter.py index d8c6df28b9..9ca7908367 100644 --- a/src/ophyd_async/tango/demo/counter.py +++ b/src/ophyd_async/tango/demo/counter.py @@ -33,10 +33,8 @@ def __init__(self, trl: str, name=""): self.add_readables([self.counts], HintedSignal.uncached) self.add_readables([self.sample_time], ConfigSignal) - def trigger(self): - return AsyncStatus(self._trigger()) - - async def _trigger(self): + @AsyncStatus.wrap + async def trigger(self) -> None: sample_time = await self.sample_time.get_value() timeout = sample_time + DEFAULT_TIMEOUT await self.start.trigger(wait=True, timeout=timeout) From 3a004ee48de985a0d41ed3bc4c8dc318c87fab53 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 26 Aug 2024 11:28:28 +0200 Subject: [PATCH 107/141] Refactored TangoReadableDevice to TangoDevice and TangoReadable. Refactored structure to for cleaner imports from tango subpackage. --- src/ophyd_async/tango/__init__.py | 44 ++++++++---- src/ophyd_async/tango/_backend/__init__.py | 22 +++++- ...{_tango_transport.py => _tango_backend.py} | 3 - .../tango/base_devices/__init__.py | 5 +- .../tango/base_devices/base_device.py | 70 +++++++++++-------- .../tango/base_devices/tango_readable.py | 23 ++++++ src/ophyd_async/tango/demo/counter.py | 6 +- src/ophyd_async/tango/demo/mover.py | 10 +-- src/ophyd_async/tango/signal/__init__.py | 6 +- src/ophyd_async/tango/signal/signal.py | 10 +-- tests/tango/test_base_device.py | 20 +++--- tests/tango/test_tango_signals.py | 4 +- tests/tango/test_tango_transport.py | 2 +- 13 files changed, 140 insertions(+), 85 deletions(-) rename src/ophyd_async/tango/_backend/{_tango_transport.py => _tango_backend.py} (99%) create mode 100644 src/ophyd_async/tango/base_devices/tango_readable.py diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py index 88276fc73c..c2da0d4214 100644 --- a/src/ophyd_async/tango/__init__.py +++ b/src/ophyd_async/tango/__init__.py @@ -1,17 +1,35 @@ -from ophyd_async.tango.base_devices import TangoReadableDevice -from ophyd_async.tango.signal import ( - tango_signal_auto, - tango_signal_r, - tango_signal_rw, - tango_signal_w, - tango_signal_x, +from ._backend import ( + AttributeProxy, + CommandProxy, + TangoSignalBackend, + ensure_proper_executor, + get_dtype_extended, + get_python_type, + get_tango_trl, + get_trl_descriptor, +) +from .base_devices import ( + TangoDevice, + TangoReadable, +) +from .signal import ( + infer_python_type, + infer_signal_frontend, + make_backend, ) __all__ = [ - "tango_signal_r", - "tango_signal_rw", - "tango_signal_w", - "tango_signal_x", - "tango_signal_auto", - "TangoReadableDevice", + TangoDevice, + TangoReadable, + TangoSignalBackend, + get_python_type, + get_dtype_extended, + get_trl_descriptor, + get_tango_trl, + infer_python_type, + infer_signal_frontend, + make_backend, + AttributeProxy, + CommandProxy, + ensure_proper_executor, ] diff --git a/src/ophyd_async/tango/_backend/__init__.py b/src/ophyd_async/tango/_backend/__init__.py index 96091f90c7..82ba7202ef 100644 --- a/src/ophyd_async/tango/_backend/__init__.py +++ b/src/ophyd_async/tango/_backend/__init__.py @@ -1,3 +1,21 @@ -from ophyd_async.tango._backend._tango_transport import TangoSignalBackend +from ._tango_backend import ( + AttributeProxy, + CommandProxy, + TangoSignalBackend, + ensure_proper_executor, + get_dtype_extended, + get_python_type, + get_tango_trl, + get_trl_descriptor, +) -__all__ = ("TangoSignalBackend",) +__all__ = [ + "AttributeProxy", + "CommandProxy", + "ensure_proper_executor", + "TangoSignalBackend", + "get_python_type", + "get_dtype_extended", + "get_trl_descriptor", + "get_tango_trl", +] diff --git a/src/ophyd_async/tango/_backend/_tango_transport.py b/src/ophyd_async/tango/_backend/_tango_backend.py similarity index 99% rename from src/ophyd_async/tango/_backend/_tango_transport.py rename to src/ophyd_async/tango/_backend/_tango_backend.py index 78a4f1820a..14cf352e7c 100644 --- a/src/ophyd_async/tango/_backend/_tango_transport.py +++ b/src/ophyd_async/tango/_backend/_tango_backend.py @@ -37,9 +37,6 @@ ) from tango.utils import is_array, is_binary, is_bool, is_float, is_int, is_str -__all__ = ["TangoSignalBackend"] - - # time constant to wait for timeout A_BIT = 1e-5 diff --git a/src/ophyd_async/tango/base_devices/__init__.py b/src/ophyd_async/tango/base_devices/__init__.py index b46b306984..9a1e7ec666 100644 --- a/src/ophyd_async/tango/base_devices/__init__.py +++ b/src/ophyd_async/tango/base_devices/__init__.py @@ -1,3 +1,4 @@ -from ophyd_async.tango.base_devices.base_device import TangoReadableDevice +from .base_device import TangoDevice +from .tango_readable import TangoReadable -__all__ = ("TangoReadableDevice",) +__all__ = ["TangoDevice", "TangoReadable"] diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/base_device.py index 71e8108077..73e1c5937f 100644 --- a/src/ophyd_async/tango/base_devices/base_device.py +++ b/src/ophyd_async/tango/base_devices/base_device.py @@ -1,54 +1,58 @@ from __future__ import annotations import asyncio -from typing import Optional +from typing import Optional, Union from ophyd_async.core import ( DEFAULT_TIMEOUT, + Device, Signal, SignalR, SignalRW, SignalW, SignalX, - StandardReadable, ) from ophyd_async.tango.signal import ( infer_python_type, infer_signal_frontend, make_backend, ) -from tango.asyncio import DeviceProxy +from tango import DeviceProxy as SyncDeviceProxy +from tango.asyncio import DeviceProxy as AsyncDeviceProxy -__all__ = ("TangoReadableDevice",) - -# -------------------------------------------------------------------- -class TangoReadableDevice(StandardReadable): +class TangoDevice(Device): """ - General class for TangoDevices. Extends StandardReadable to provide - attributes for Tango devices. - - Usage: to proper signals mount should be awaited: - new_device = await TangoDevice() - - attributes: - trl: Tango resource locator, typically of the device server. - proxy: DeviceProxy object for the device. This is created when the device - is connected. - src_dict: Dictionary of the device's attributes. This can be used to account - for variation in attribute names across similar servers. + General class for TangoDevices. Extends Device to provide attributes for Tango + devices. + + Parameters + ---------- + trl: str + Tango resource locator, typically of the device server. + device_proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] + Asynchronous or synchronous DeviceProxy object for the device. If not provided, + an asynchronous DeviceProxy object will be created using the trl and awaited + when the device is connected. """ - src_dict: dict = {} trl: str = "" - proxy: Optional[DeviceProxy] = None + proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] = None + + def __init__( + self, + trl: Optional[str] = None, + device_proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] = None, + name: str = "", + ) -> None: + if not trl and not device_proxy: + raise ValueError("Either 'trl' or 'device_proxy' must be provided.") + + self.trl = trl if trl else "" + self.proxy = device_proxy if device_proxy else AsyncDeviceProxy(trl) - # -------------------------------------------------------------------- - def __init__(self, trl: str, name="") -> None: - self.trl = trl - self.proxy: Optional[DeviceProxy] = None self.create_children_from_annotations() - StandardReadable.__init__(self, name=name) + super().__init__(name=name) async def connect( self, @@ -57,10 +61,13 @@ async def connect( force_reconnect: bool = False, ): async def closure(): - if self.proxy is None: - self.proxy = await DeviceProxy(self.trl) - elif isinstance(self.proxy, asyncio.Future): - self.proxy = await self.proxy + try: + if self.proxy is None: + self.proxy = await AsyncDeviceProxy(self.trl) + elif isinstance(self.proxy, asyncio.Future): + self.proxy = await self.proxy + except Exception as e: + raise RuntimeError("Could not connect to device proxy") from e return self await closure() @@ -111,3 +118,6 @@ def create_children_from_annotations(self): else: print(obj_type, type(obj_type)) raise ValueError(f"Invalid signal type {obj_type}") + + +# -------------------------------------------------------------------- diff --git a/src/ophyd_async/tango/base_devices/tango_readable.py b/src/ophyd_async/tango/base_devices/tango_readable.py new file mode 100644 index 0000000000..27c1e5477b --- /dev/null +++ b/src/ophyd_async/tango/base_devices/tango_readable.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from ophyd_async.core import StandardReadable +from ophyd_async.tango.base_devices.base_device import TangoDevice + + +class TangoReadable(TangoDevice, StandardReadable): + """ + General class for readable TangoDevices. Extends StandardReadable to provide + attributes for Tango devices. + + Usage: to proper signals mount should be awaited: + new_device = await TangoDevice() + + attributes: + trl: Tango resource locator, typically of the device server. + proxy: AsyncDeviceProxy object for the device. This is created when the + device is connected. + """ + + # -------------------------------------------------------------------- + def __init__(self, trl: str, name="") -> None: + TangoDevice.__init__(self, trl, name=name) diff --git a/src/ophyd_async/tango/demo/counter.py b/src/ophyd_async/tango/demo/counter.py index 9ca7908367..ed0009ea0f 100644 --- a/src/ophyd_async/tango/demo/counter.py +++ b/src/ophyd_async/tango/demo/counter.py @@ -9,9 +9,7 @@ Signal, SignalX, ) -from ophyd_async.tango import ( - TangoReadableDevice, -) +from ophyd_async.tango import TangoReadable @dataclass @@ -19,7 +17,7 @@ class TangoCounterConfig: sample_time: Optional[float] = None -class TangoCounter(TangoReadableDevice): +class TangoCounter(TangoReadable): # Enter the name and type of the signals you want to use # If type is None or Signal, the type will be inferred from the Tango device counts: None diff --git a/src/ophyd_async/tango/demo/mover.py b/src/ophyd_async/tango/demo/mover.py index cc4eb7ee35..69c31dfc81 100644 --- a/src/ophyd_async/tango/demo/mover.py +++ b/src/ophyd_async/tango/demo/mover.py @@ -18,12 +18,8 @@ WatcherUpdate, observe_value, ) -from ophyd_async.tango import ( - TangoReadableDevice, -) -from tango import ( - DevState, -) +from ophyd_async.tango import TangoReadable +from tango import DevState @dataclass @@ -31,7 +27,7 @@ class TangoMoverConfig: velocity: Optional[float] = None -class TangoMover(TangoReadableDevice, Movable, Stoppable): +class TangoMover(TangoReadable, Movable, Stoppable): # Enter the name and type of the signals you want to use # If type is None or Signal, the type will be inferred from the Tango device position: SignalRW diff --git a/src/ophyd_async/tango/signal/__init__.py b/src/ophyd_async/tango/signal/__init__.py index 6f5804db5d..d5e45b2c73 100644 --- a/src/ophyd_async/tango/signal/__init__.py +++ b/src/ophyd_async/tango/signal/__init__.py @@ -10,12 +10,12 @@ ) __all__ = ( + "infer_python_type", + "infer_signal_frontend", + "make_backend", "tango_signal_r", "tango_signal_rw", "tango_signal_w", "tango_signal_x", "tango_signal_auto", - "make_backend", - "infer_python_type", - "infer_signal_frontend", ) diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/signal.py index 90a87fe936..3740abf9b3 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/signal.py @@ -8,7 +8,7 @@ import numpy.typing as npt from ophyd_async.core import DEFAULT_TIMEOUT, SignalR, SignalRW, SignalW, SignalX, T -from ophyd_async.tango._backend._tango_transport import ( +from ophyd_async.tango._backend._tango_backend import ( TangoSignalBackend, get_python_type, ) @@ -16,14 +16,6 @@ from tango import DeviceProxy as SyncDeviceProxy from tango.asyncio import DeviceProxy -__all__ = ( - "tango_signal_rw", - "tango_signal_r", - "tango_signal_w", - "tango_signal_x", - "tango_signal_auto", -) - def make_backend( datatype: Optional[Type[T]], diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 6acb156583..60bfbdd37a 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -9,11 +9,13 @@ from bluesky import RunEngine from ophyd_async.core import DeviceCollector, T -from ophyd_async.tango import TangoReadableDevice, tango_signal_auto -from ophyd_async.tango._backend._tango_transport import get_python_type -from ophyd_async.tango.demo._tango.servers import DemoCounter, DemoMover -from ophyd_async.tango.demo.counter import TangoCounter -from ophyd_async.tango.demo.mover import TangoMover +from ophyd_async.tango import TangoReadable, get_python_type, tango_signal_auto +from ophyd_async.tango.demo import ( + DemoCounter, + DemoMover, + TangoCounter, + TangoMover, +) from tango import ( AttrDataFormat, AttrQuality, @@ -169,12 +171,12 @@ def raise_exception_cmd(self): # -------------------------------------------------------------------- -class TestReadableDevice(TangoReadableDevice): +class TestTangoReadable(TangoReadable): __test__ = False def __init__(self, trl: str, name="") -> None: self.trl = trl - TangoReadableDevice.__init__(self, trl, name) + TangoReadable.__init__(self, trl, name) def register_signals(self): for feature in TESTED_FEATURES: @@ -310,7 +312,7 @@ async def test_connect(tango_test_device): values, description = await describe_class(tango_test_device) async with DeviceCollector(): - test_device = TestReadableDevice(tango_test_device) + test_device = TestTangoReadable(tango_test_device) assert test_device.name == "test_device" assert description == await test_device.describe() @@ -322,7 +324,7 @@ async def test_connect(tango_test_device): async def test_with_bluesky(tango_test_device): async def connect(): async with DeviceCollector(): - device = TestReadableDevice(tango_test_device) + device = TestTangoReadable(tango_test_device) return device ophyd_dev = await connect() diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index da614c7d9b..4fcda01833 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -12,8 +12,8 @@ from test_base_device import TestDevice from ophyd_async.core import SignalBackend, SignalRW, SignalX, T -from ophyd_async.tango._backend import TangoSignalBackend -from ophyd_async.tango.signal import ( +from ophyd_async.tango import ( + TangoSignalBackend, tango_signal_auto, tango_signal_r, tango_signal_rw, diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index 79720290b5..382344bafa 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -11,7 +11,7 @@ prepare_device, ) -from ophyd_async.tango._backend._tango_transport import ( +from ophyd_async.tango import ( AttributeProxy, CommandProxy, TangoSignalBackend, From 9b526fdffb2011059586e7dbe8d7c24b4545d219 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 26 Aug 2024 13:26:49 +0200 Subject: [PATCH 108/141] Added class decorator tango_polling to disable event driven updates in favour of polling. This is useful for tango device servers that don't support events. --- src/ophyd_async/tango/__init__.py | 12 +++++++ .../tango/base_devices/__init__.py | 4 +-- .../tango/base_devices/tango_readable.py | 34 ++++++++++++++++++- src/ophyd_async/tango/demo/README.md | 9 ++--- src/ophyd_async/tango/demo/mover.py | 3 +- tests/tango/test_base_device.py | 7 ---- 6 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py index c2da0d4214..ca329bec7e 100644 --- a/src/ophyd_async/tango/__init__.py +++ b/src/ophyd_async/tango/__init__.py @@ -11,16 +11,23 @@ from .base_devices import ( TangoDevice, TangoReadable, + tango_polling, ) from .signal import ( infer_python_type, infer_signal_frontend, make_backend, + tango_signal_auto, + tango_signal_r, + tango_signal_rw, + tango_signal_w, + tango_signal_x, ) __all__ = [ TangoDevice, TangoReadable, + tango_polling, TangoSignalBackend, get_python_type, get_dtype_extended, @@ -32,4 +39,9 @@ AttributeProxy, CommandProxy, ensure_proper_executor, + tango_signal_auto, + tango_signal_r, + tango_signal_rw, + tango_signal_w, + tango_signal_x, ] diff --git a/src/ophyd_async/tango/base_devices/__init__.py b/src/ophyd_async/tango/base_devices/__init__.py index 9a1e7ec666..b2a2ba0954 100644 --- a/src/ophyd_async/tango/base_devices/__init__.py +++ b/src/ophyd_async/tango/base_devices/__init__.py @@ -1,4 +1,4 @@ from .base_device import TangoDevice -from .tango_readable import TangoReadable +from .tango_readable import TangoReadable, tango_polling -__all__ = ["TangoDevice", "TangoReadable"] +__all__ = ["TangoDevice", "TangoReadable", "tango_polling"] diff --git a/src/ophyd_async/tango/base_devices/tango_readable.py b/src/ophyd_async/tango/base_devices/tango_readable.py index 27c1e5477b..7f411a115a 100644 --- a/src/ophyd_async/tango/base_devices/tango_readable.py +++ b/src/ophyd_async/tango/base_devices/tango_readable.py @@ -1,9 +1,29 @@ from __future__ import annotations -from ophyd_async.core import StandardReadable +from typing import Tuple + +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + ConfigSignal, + HintedSignal, + StandardReadable, +) from ophyd_async.tango.base_devices.base_device import TangoDevice +def tango_polling(*args): + """ + Class decorator to set polling for Tango devices. This is useful for device servers + that do not support event-driven updates. + """ + + def decorator(cls): + cls._polling = (True, *args) + return cls + + return decorator + + class TangoReadable(TangoDevice, StandardReadable): """ General class for readable TangoDevices. Extends StandardReadable to provide @@ -19,5 +39,17 @@ class TangoReadable(TangoDevice, StandardReadable): """ # -------------------------------------------------------------------- + _polling: Tuple = (False, 0.1, None, 0.1) + def __init__(self, trl: str, name="") -> None: TangoDevice.__init__(self, trl, name=name) + + async def connect(self, mock=False, timeout=DEFAULT_TIMEOUT, force_reconnect=False): + await super().connect(mock=mock, timeout=timeout) + if self._polling[0]: + for sig in self._readables: + if isinstance(sig, HintedSignal) or isinstance(sig, ConfigSignal): + backend = sig.signal._backend # noqa: SLF001 + else: + backend = sig._backend # noqa: SLF001 + backend.set_polling(*self._polling) diff --git a/src/ophyd_async/tango/demo/README.md b/src/ophyd_async/tango/demo/README.md index d41c160f15..af1fa6071d 100644 --- a/src/ophyd_async/tango/demo/README.md +++ b/src/ophyd_async/tango/demo/README.md @@ -32,7 +32,9 @@ content = ( ], } ) + tango_context = MultiDeviceTestContext(content) + with tango_context as context: motor1 = TangoMover(trl=context.get_device_access("demo/motor/1"), name="motor1") counter1 = TangoCounter(trl=context.get_device_access("demo/counter/1"), name="counter1") @@ -41,13 +43,6 @@ with tango_context as context: await counter1.connect() await counter2.connect() - # Events are not supported by the test context so we disable them - motor1.position._backend.allow_events(False) - motor1.state._backend.allow_events(False) - # Enable polling for the position and state attributes - motor1.position._backend.set_polling(True, 0.1, 0.1) - motor1.state._backend.set_polling(True, 0.1) - RE = RunEngine() RE.subscribe(BestEffortCallback()) RE.waiting_hook = ProgressBarManager() diff --git a/src/ophyd_async/tango/demo/mover.py b/src/ophyd_async/tango/demo/mover.py index 69c31dfc81..7f6805b7ea 100644 --- a/src/ophyd_async/tango/demo/mover.py +++ b/src/ophyd_async/tango/demo/mover.py @@ -18,7 +18,7 @@ WatcherUpdate, observe_value, ) -from ophyd_async.tango import TangoReadable +from ophyd_async.tango import TangoReadable, tango_polling from tango import DevState @@ -27,6 +27,7 @@ class TangoMoverConfig: velocity: Optional[float] = None +@tango_polling(0.1, 0.1, 0.1) class TangoMover(TangoReadable, Movable, Stoppable): # Enter the name and type of the signals you want to use # If type is None or Signal, the type will be inferred from the Tango device diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 60bfbdd37a..0373284a30 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -354,13 +354,6 @@ async def test_tango_demo(demo_test_context): await counter1.connect() await counter2.connect() - # Events are not supported by the test context so we disable them - motor1.position._backend.allow_events(False) - motor1.state._backend.allow_events(False) - # Enable polling for the position and state attributes - motor1.position._backend.set_polling(True, 0.1, 0.1) - motor1.state._backend.set_polling(True, 0.1) - RE = RunEngine() RE(bps.read(motor1.position)) RE(bp.count([counter1, counter2])) From e9f909a18172477ce5f169425d0b7c26f870aca1 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 26 Aug 2024 13:35:19 +0200 Subject: [PATCH 109/141] Refactored to move _backend subpackage to signal. Renamed modules to better resemble other parts of codebase. --- src/ophyd_async/tango/__init__.py | 14 ++++++------- src/ophyd_async/tango/_backend/__init__.py | 21 ------------------- .../tango/base_devices/__init__.py | 4 ++-- .../{base_device.py => _base_device.py} | 0 .../{tango_readable.py => _tango_readable.py} | 2 +- src/ophyd_async/tango/demo/__init__.py | 4 ++-- .../tango/demo/{counter.py => _counter.py} | 0 .../tango/demo/{mover.py => _mover.py} | 0 src/ophyd_async/tango/demo/_tango/__init__.py | 2 +- .../demo/_tango/{servers.py => _servers.py} | 0 src/ophyd_async/tango/signal/__init__.py | 20 +++++++++++++++++- .../tango/signal/{signal.py => _signal.py} | 2 +- .../_tango_transport.py} | 0 13 files changed, 32 insertions(+), 37 deletions(-) delete mode 100644 src/ophyd_async/tango/_backend/__init__.py rename src/ophyd_async/tango/base_devices/{base_device.py => _base_device.py} (100%) rename src/ophyd_async/tango/base_devices/{tango_readable.py => _tango_readable.py} (96%) rename src/ophyd_async/tango/demo/{counter.py => _counter.py} (100%) rename src/ophyd_async/tango/demo/{mover.py => _mover.py} (100%) rename src/ophyd_async/tango/demo/_tango/{servers.py => _servers.py} (100%) rename src/ophyd_async/tango/signal/{signal.py => _signal.py} (99%) rename src/ophyd_async/tango/{_backend/_tango_backend.py => signal/_tango_transport.py} (100%) diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py index ca329bec7e..3f8ef2d0d9 100644 --- a/src/ophyd_async/tango/__init__.py +++ b/src/ophyd_async/tango/__init__.py @@ -1,4 +1,9 @@ -from ._backend import ( +from .base_devices import ( + TangoDevice, + TangoReadable, + tango_polling, +) +from .signal import ( AttributeProxy, CommandProxy, TangoSignalBackend, @@ -7,13 +12,6 @@ get_python_type, get_tango_trl, get_trl_descriptor, -) -from .base_devices import ( - TangoDevice, - TangoReadable, - tango_polling, -) -from .signal import ( infer_python_type, infer_signal_frontend, make_backend, diff --git a/src/ophyd_async/tango/_backend/__init__.py b/src/ophyd_async/tango/_backend/__init__.py deleted file mode 100644 index 82ba7202ef..0000000000 --- a/src/ophyd_async/tango/_backend/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -from ._tango_backend import ( - AttributeProxy, - CommandProxy, - TangoSignalBackend, - ensure_proper_executor, - get_dtype_extended, - get_python_type, - get_tango_trl, - get_trl_descriptor, -) - -__all__ = [ - "AttributeProxy", - "CommandProxy", - "ensure_proper_executor", - "TangoSignalBackend", - "get_python_type", - "get_dtype_extended", - "get_trl_descriptor", - "get_tango_trl", -] diff --git a/src/ophyd_async/tango/base_devices/__init__.py b/src/ophyd_async/tango/base_devices/__init__.py index b2a2ba0954..9f4e77e50e 100644 --- a/src/ophyd_async/tango/base_devices/__init__.py +++ b/src/ophyd_async/tango/base_devices/__init__.py @@ -1,4 +1,4 @@ -from .base_device import TangoDevice -from .tango_readable import TangoReadable, tango_polling +from ._base_device import TangoDevice +from ._tango_readable import TangoReadable, tango_polling __all__ = ["TangoDevice", "TangoReadable", "tango_polling"] diff --git a/src/ophyd_async/tango/base_devices/base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py similarity index 100% rename from src/ophyd_async/tango/base_devices/base_device.py rename to src/ophyd_async/tango/base_devices/_base_device.py diff --git a/src/ophyd_async/tango/base_devices/tango_readable.py b/src/ophyd_async/tango/base_devices/_tango_readable.py similarity index 96% rename from src/ophyd_async/tango/base_devices/tango_readable.py rename to src/ophyd_async/tango/base_devices/_tango_readable.py index 7f411a115a..722262bc7e 100644 --- a/src/ophyd_async/tango/base_devices/tango_readable.py +++ b/src/ophyd_async/tango/base_devices/_tango_readable.py @@ -8,7 +8,7 @@ HintedSignal, StandardReadable, ) -from ophyd_async.tango.base_devices.base_device import TangoDevice +from ophyd_async.tango.base_devices._base_device import TangoDevice def tango_polling(*args): diff --git a/src/ophyd_async/tango/demo/__init__.py b/src/ophyd_async/tango/demo/__init__.py index 4c9cb10a23..4334b401f6 100644 --- a/src/ophyd_async/tango/demo/__init__.py +++ b/src/ophyd_async/tango/demo/__init__.py @@ -1,6 +1,6 @@ +from ._counter import TangoCounter, TangoCounterConfig +from ._mover import TangoMover, TangoMoverConfig from ._tango import DemoCounter, DemoMover -from .counter import TangoCounter, TangoCounterConfig -from .mover import TangoMover, TangoMoverConfig __all__ = [ "DemoCounter", diff --git a/src/ophyd_async/tango/demo/counter.py b/src/ophyd_async/tango/demo/_counter.py similarity index 100% rename from src/ophyd_async/tango/demo/counter.py rename to src/ophyd_async/tango/demo/_counter.py diff --git a/src/ophyd_async/tango/demo/mover.py b/src/ophyd_async/tango/demo/_mover.py similarity index 100% rename from src/ophyd_async/tango/demo/mover.py rename to src/ophyd_async/tango/demo/_mover.py diff --git a/src/ophyd_async/tango/demo/_tango/__init__.py b/src/ophyd_async/tango/demo/_tango/__init__.py index 10881b4e77..fd7965a5f1 100644 --- a/src/ophyd_async/tango/demo/_tango/__init__.py +++ b/src/ophyd_async/tango/demo/_tango/__init__.py @@ -1,3 +1,3 @@ -from .servers import DemoCounter, DemoMover +from ._servers import DemoCounter, DemoMover __all__ = ["DemoCounter", "DemoMover"] diff --git a/src/ophyd_async/tango/demo/_tango/servers.py b/src/ophyd_async/tango/demo/_tango/_servers.py similarity index 100% rename from src/ophyd_async/tango/demo/_tango/servers.py rename to src/ophyd_async/tango/demo/_tango/_servers.py diff --git a/src/ophyd_async/tango/signal/__init__.py b/src/ophyd_async/tango/signal/__init__.py index d5e45b2c73..bf8abbcbba 100644 --- a/src/ophyd_async/tango/signal/__init__.py +++ b/src/ophyd_async/tango/signal/__init__.py @@ -1,4 +1,4 @@ -from .signal import ( +from ._signal import ( infer_python_type, infer_signal_frontend, make_backend, @@ -8,8 +8,26 @@ tango_signal_w, tango_signal_x, ) +from ._tango_transport import ( + AttributeProxy, + CommandProxy, + TangoSignalBackend, + ensure_proper_executor, + get_dtype_extended, + get_python_type, + get_tango_trl, + get_trl_descriptor, +) __all__ = ( + "AttributeProxy", + "CommandProxy", + "ensure_proper_executor", + "TangoSignalBackend", + "get_python_type", + "get_dtype_extended", + "get_trl_descriptor", + "get_tango_trl", "infer_python_type", "infer_signal_frontend", "make_backend", diff --git a/src/ophyd_async/tango/signal/signal.py b/src/ophyd_async/tango/signal/_signal.py similarity index 99% rename from src/ophyd_async/tango/signal/signal.py rename to src/ophyd_async/tango/signal/_signal.py index 3740abf9b3..2f5d4f259c 100644 --- a/src/ophyd_async/tango/signal/signal.py +++ b/src/ophyd_async/tango/signal/_signal.py @@ -8,7 +8,7 @@ import numpy.typing as npt from ophyd_async.core import DEFAULT_TIMEOUT, SignalR, SignalRW, SignalW, SignalX, T -from ophyd_async.tango._backend._tango_backend import ( +from ophyd_async.tango.signal._tango_transport import ( TangoSignalBackend, get_python_type, ) diff --git a/src/ophyd_async/tango/_backend/_tango_backend.py b/src/ophyd_async/tango/signal/_tango_transport.py similarity index 100% rename from src/ophyd_async/tango/_backend/_tango_backend.py rename to src/ophyd_async/tango/signal/_tango_transport.py From 7c69f55376c6835b906807f0bd6f8e6e279e305c Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 26 Aug 2024 15:43:39 +0200 Subject: [PATCH 110/141] Moved setting polling parameters of backend to TangoDevice::register_signals. Polling and event support cannot be changed at runtime. These must be set before the backend is connected to the device server. Improved coverage of tango demo. --- .../tango/base_devices/__init__.py | 4 +-- .../tango/base_devices/_base_device.py | 15 +++++++++ .../tango/base_devices/_tango_readable.py | 26 ---------------- src/ophyd_async/tango/demo/_counter.py | 12 ++++--- src/ophyd_async/tango/demo/_mover.py | 17 +++++----- .../tango/signal/_tango_transport.py | 12 +------ tests/tango/test_base_device.py | 31 +++++++++++++++++-- tests/tango/test_tango_transport.py | 6 ---- 8 files changed, 60 insertions(+), 63 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/__init__.py b/src/ophyd_async/tango/base_devices/__init__.py index 9f4e77e50e..ecba4e1b23 100644 --- a/src/ophyd_async/tango/base_devices/__init__.py +++ b/src/ophyd_async/tango/base_devices/__init__.py @@ -1,4 +1,4 @@ -from ._base_device import TangoDevice -from ._tango_readable import TangoReadable, tango_polling +from ._base_device import TangoDevice, tango_polling +from ._tango_readable import TangoReadable __all__ = ["TangoDevice", "TangoReadable", "tango_polling"] diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index 73e1c5937f..9e5b1667c5 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -38,6 +38,7 @@ class TangoDevice(Device): trl: str = "" proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] = None + _polling: tuple = (False, 0.1, None, 0.1) def __init__( self, @@ -91,6 +92,9 @@ def register_signals(self): write_trl=read_trl, device_proxy=self.proxy, ) + if self._polling[0]: + backend.allow_events(False) + backend.set_polling(*self._polling) signal._backend = backend # noqa: SLF001 def create_children_from_annotations(self): @@ -121,3 +125,14 @@ def create_children_from_annotations(self): # -------------------------------------------------------------------- +def tango_polling(*args): + """ + Class decorator to set polling for Tango devices. This is useful for device servers + that do not support event-driven updates. + """ + + def decorator(cls): + cls._polling = (True, *args) + return cls + + return decorator diff --git a/src/ophyd_async/tango/base_devices/_tango_readable.py b/src/ophyd_async/tango/base_devices/_tango_readable.py index 722262bc7e..14bd79b7cd 100644 --- a/src/ophyd_async/tango/base_devices/_tango_readable.py +++ b/src/ophyd_async/tango/base_devices/_tango_readable.py @@ -3,27 +3,11 @@ from typing import Tuple from ophyd_async.core import ( - DEFAULT_TIMEOUT, - ConfigSignal, - HintedSignal, StandardReadable, ) from ophyd_async.tango.base_devices._base_device import TangoDevice -def tango_polling(*args): - """ - Class decorator to set polling for Tango devices. This is useful for device servers - that do not support event-driven updates. - """ - - def decorator(cls): - cls._polling = (True, *args) - return cls - - return decorator - - class TangoReadable(TangoDevice, StandardReadable): """ General class for readable TangoDevices. Extends StandardReadable to provide @@ -43,13 +27,3 @@ class TangoReadable(TangoDevice, StandardReadable): def __init__(self, trl: str, name="") -> None: TangoDevice.__init__(self, trl, name=name) - - async def connect(self, mock=False, timeout=DEFAULT_TIMEOUT, force_reconnect=False): - await super().connect(mock=mock, timeout=timeout) - if self._polling[0]: - for sig in self._readables: - if isinstance(sig, HintedSignal) or isinstance(sig, ConfigSignal): - backend = sig.signal._backend # noqa: SLF001 - else: - backend = sig._backend # noqa: SLF001 - backend.set_polling(*self._polling) diff --git a/src/ophyd_async/tango/demo/_counter.py b/src/ophyd_async/tango/demo/_counter.py index ed0009ea0f..d8387351ce 100644 --- a/src/ophyd_async/tango/demo/_counter.py +++ b/src/ophyd_async/tango/demo/_counter.py @@ -37,12 +37,14 @@ async def trigger(self) -> None: timeout = sample_time + DEFAULT_TIMEOUT await self.start.trigger(wait=True, timeout=timeout) - def prepare(self, value: TangoCounterConfig) -> AsyncStatus: - return AsyncStatus(self._prepare(value)) - - async def _prepare(self, value: TangoCounterConfig) -> None: + @AsyncStatus.wrap + async def prepare(self, value: TangoCounterConfig) -> None: config = value.__dataclass_fields__ - for key, v in config.items(): + for key in config: + v = getattr(value, key) if v is not None: if hasattr(self, key): await getattr(self, key).set(v) + + def get_dataclass(self) -> TangoCounterConfig: + return TangoCounterConfig() diff --git a/src/ophyd_async/tango/demo/_mover.py b/src/ophyd_async/tango/demo/_mover.py index 7f6805b7ea..c7d1c6f0df 100644 --- a/src/ophyd_async/tango/demo/_mover.py +++ b/src/ophyd_async/tango/demo/_mover.py @@ -42,10 +42,8 @@ def __init__(self, trl: str, name=""): self.add_readables([self.velocity], ConfigSignal) self._set_success = True - def set(self, value: float, timeout: CalculatableTimeout = CalculateTimeout): - return WatchableAsyncStatus(self._set(value, timeout)) - - async def _set(self, value: float, timeout: CalculatableTimeout = CalculateTimeout): + @WatchableAsyncStatus.wrap + async def set(self, value: float, timeout: CalculatableTimeout = CalculateTimeout): self._set_success = True (old_position, velocity) = await asyncio.gather( self.position.get_value(), self.velocity.get_value() @@ -94,15 +92,14 @@ def stop(self, success: bool = True) -> AsyncStatus: self._set_success = success return self._stop.trigger() - def prepare(self, value: TangoMoverConfig) -> AsyncStatus: - return AsyncStatus(self._prepare(value)) - - async def _prepare(self, value: TangoMoverConfig) -> None: + @AsyncStatus.wrap + async def prepare(self, value: TangoMoverConfig) -> None: config = value.__dataclass_fields__ - for key, v in config.items(): + for key in config: + v = getattr(value, key) if v is not None: if hasattr(self, key): await getattr(self, key).set(v) - async def get_dataclass(self) -> TangoMoverConfig: + def get_dataclass(self) -> TangoMoverConfig: return TangoMoverConfig() diff --git a/src/ophyd_async/tango/signal/_tango_transport.py b/src/ophyd_async/tango/signal/_tango_transport.py index 14cf352e7c..dea54c8fb9 100644 --- a/src/ophyd_async/tango/signal/_tango_transport.py +++ b/src/ophyd_async/tango/signal/_tango_transport.py @@ -688,10 +688,7 @@ async def get_setpoint(self) -> T: # -------------------------------------------------------------------- def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: - if ( - self.proxies[self.read_trl].support_events is False - and self.polling[0] is False - ): + if self.support_events is False and self.polling[0] is False: raise RuntimeError( f"Cannot set event for {self.read_trl}. " "Cannot set a callback on an attribute that does not support events and" @@ -720,14 +717,7 @@ def set_polling( rel_change=0.1, ): self.polling = (allow_polling, polling_period, abs_change, rel_change) - if self.proxies[self.read_trl] is not None: - self.proxies[self.read_trl].set_polling( - allow_polling, polling_period, abs_change, rel_change - ) # -------------------------------------------------------------------- def allow_events(self, allow: Optional[bool] = True): self.support_events = allow - if self.proxies[self.read_trl] is not None: - self.proxies[self.read_trl].support_events = self.support_events - return self.proxies[self.read_trl].support_events diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 0373284a30..e27ebb6ce8 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -1,3 +1,4 @@ +import asyncio import time from enum import Enum, IntEnum from typing import Type @@ -186,6 +187,8 @@ def register_signals(self): tango_signal_auto(datatype=None, trl=f"{self.trl}/{feature}"), ) attr = getattr(self, feature) + attr._backend.allow_events(False) + attr._backend.set_polling(0.1, 0.1, 0.1) self.add_readables([attr]) @@ -331,9 +334,6 @@ async def connect(): # now let's do some bluesky stuff RE = RunEngine() - for readable in ophyd_dev._readables: - readable._backend.allow_events(False) - readable._backend.set_polling(True, 0.1, 0.1) RE(bp.count([ophyd_dev], 1)) @@ -355,5 +355,30 @@ async def test_tango_demo(demo_test_context): await counter2.connect() RE = RunEngine() + + dc = motor1.get_dataclass() + dc.velocity = 1.0 + prepare_status = motor1.prepare(dc) + await prepare_status + assert all([prepare_status.done, prepare_status.success]) + + cc = counter1.get_dataclass() + cc.sample_time = 0.1 + prepare_status1 = counter1.prepare(cc) + prepare_status2 = counter2.prepare(cc) + await prepare_status1 + await prepare_status2 + assert all([prepare_status1.done, prepare_status1.success]) + assert all([prepare_status2.done, prepare_status2.success]) + RE(bps.read(motor1.position)) + RE(bps.mv(motor1, 0)) RE(bp.count([counter1, counter2])) + + set_status = motor1.set(1.0) + await asyncio.sleep(0.1) + stop_status = motor1.stop() + await set_status + await stop_status + assert all([set_status.done, stop_status.done]) + assert all([set_status.success, stop_status.success]) diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index 382344bafa..2a7c80e2ab 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -685,7 +685,6 @@ def callback(reading, value): # Try to add a non-callable callback transport.allow_events(True) - transport.set_callback(None) with pytest.raises(RuntimeError) as exc_info: transport.set_callback(1) assert "Callback must be a callable" in str(exc_info.value) @@ -694,13 +693,8 @@ def callback(reading, value): # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_tango_transport_set_polling(transport): - source = transport.source("") transport.set_polling(True, 0.1, 1, 0.1) assert transport.polling == (True, 0.1, 1, 0.1) - assert transport.proxies[source]._allow_polling - assert transport.proxies[source]._polling_period == 0.1 - assert transport.proxies[source]._abs_change == 1 - assert transport.proxies[source]._rel_change == 0.1 # -------------------------------------------------------------------- From e8dfa71db37cb1fa2ae1f447f5a4479c551ff7fb Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 27 Aug 2024 10:16:47 +0200 Subject: [PATCH 111/141] Improved coverage of base device testing. --- .../tango/base_devices/_base_device.py | 9 ++-- .../tango/base_devices/_tango_readable.py | 13 ++++-- tests/tango/test_base_device.py | 43 ++++++++++++++++--- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index 9e5b1667c5..66f990de5e 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -1,6 +1,5 @@ from __future__ import annotations -import asyncio from typing import Optional, Union from ophyd_async.core import ( @@ -50,7 +49,7 @@ def __init__( raise ValueError("Either 'trl' or 'device_proxy' must be provided.") self.trl = trl if trl else "" - self.proxy = device_proxy if device_proxy else AsyncDeviceProxy(trl) + self.proxy = device_proxy self.create_children_from_annotations() super().__init__(name=name) @@ -65,12 +64,13 @@ async def closure(): try: if self.proxy is None: self.proxy = await AsyncDeviceProxy(self.trl) - elif isinstance(self.proxy, asyncio.Future): - self.proxy = await self.proxy except Exception as e: raise RuntimeError("Could not connect to device proxy") from e return self + if self.trl in ["", None]: + self.trl = self.proxy.name() + await closure() self.register_signals() # set_name should be called again to propagate the new signal names @@ -120,7 +120,6 @@ def create_children_from_annotations(self): infer_signal_frontend(trl=f"{self.trl}/" f"{tango_name}"), ) else: - print(obj_type, type(obj_type)) raise ValueError(f"Invalid signal type {obj_type}") diff --git a/src/ophyd_async/tango/base_devices/_tango_readable.py b/src/ophyd_async/tango/base_devices/_tango_readable.py index 14bd79b7cd..a168618717 100644 --- a/src/ophyd_async/tango/base_devices/_tango_readable.py +++ b/src/ophyd_async/tango/base_devices/_tango_readable.py @@ -1,11 +1,13 @@ from __future__ import annotations -from typing import Tuple +from typing import Optional, Tuple, Union from ophyd_async.core import ( StandardReadable, ) from ophyd_async.tango.base_devices._base_device import TangoDevice +from tango import DeviceProxy as SyncDeviceProxy +from tango.asyncio import DeviceProxy as AsyncDeviceProxy class TangoReadable(TangoDevice, StandardReadable): @@ -25,5 +27,10 @@ class TangoReadable(TangoDevice, StandardReadable): # -------------------------------------------------------------------- _polling: Tuple = (False, 0.1, None, 0.1) - def __init__(self, trl: str, name="") -> None: - TangoDevice.__init__(self, trl, name=name) + def __init__( + self, + trl: Optional[str] = None, + device_proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] = None, + name: str = "", + ) -> None: + TangoDevice.__init__(self, trl, device_proxy=device_proxy, name=name) diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index e27ebb6ce8..9e947732d0 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -1,7 +1,7 @@ import asyncio import time from enum import Enum, IntEnum -from typing import Type +from typing import Optional, Type, Union import bluesky.plan_stubs as bps import bluesky.plans as bp @@ -9,6 +9,7 @@ import pytest from bluesky import RunEngine +import tango from ophyd_async.core import DeviceCollector, T from ophyd_async.tango import TangoReadable, get_python_type, tango_signal_auto from ophyd_async.tango.demo import ( @@ -24,7 +25,8 @@ CmdArgType, DevState, ) -from tango.asyncio import DeviceProxy +from tango import DeviceProxy as SyncDeviceProxy +from tango.asyncio import DeviceProxy as AsyncDeviceProxy from tango.asyncio_executor import set_global_executor from tango.server import Device, attribute, command from tango.test_context import MultiDeviceTestContext @@ -175,9 +177,14 @@ def raise_exception_cmd(self): class TestTangoReadable(TangoReadable): __test__ = False - def __init__(self, trl: str, name="") -> None: + def __init__( + self, + trl: Optional[str] = None, + device_proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] = None, + name: str = "", + ) -> None: self.trl = trl - TangoReadable.__init__(self, trl, name) + TangoReadable.__init__(self, trl, device_proxy, name) def register_signals(self): for feature in TESTED_FEATURES: @@ -196,7 +203,7 @@ def register_signals(self): async def describe_class(fqtrl): description = {} values = {} - dev = await DeviceProxy(fqtrl) + dev = await AsyncDeviceProxy(fqtrl) for name in TESTED_FEATURES: if name in dev.get_attribute_list(): @@ -314,6 +321,11 @@ def compare_values(expected, received): async def test_connect(tango_test_device): values, description = await describe_class(tango_test_device) + with pytest.raises(ValueError) as excinfo: + async with DeviceCollector(): + TestTangoReadable() + assert "Either 'trl' or 'device_proxy' must be provided." in str(excinfo.value) + async with DeviceCollector(): test_device = TestTangoReadable(tango_test_device) @@ -322,6 +334,27 @@ async def test_connect(tango_test_device): compare_values(values, await test_device.read()) +# -------------------------------------------------------------------- +@pytest.mark.asyncio +@pytest.mark.parametrize("proxy", [True, False, None]) +async def test_connect_proxy(tango_test_device, proxy: Optional[bool]): + if proxy is None: + test_device = TestTangoReadable(trl=tango_test_device) + test_device.proxy = None + await test_device.connect() + assert isinstance(test_device.proxy, tango._tango.DeviceProxy) + elif proxy: + proxy = await AsyncDeviceProxy(tango_test_device) + test_device = TestTangoReadable(device_proxy=proxy) + await test_device.connect() + assert isinstance(test_device.proxy, tango._tango.DeviceProxy) + else: + proxy = SyncDeviceProxy(tango_test_device) + test_device = TestTangoReadable(device_proxy=proxy) + await test_device.connect() + assert isinstance(test_device.proxy, tango._tango.DeviceProxy) + + # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_with_bluesky(tango_test_device): From 72d4d1d682f7bfa24866c407993677de3c4c6280 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 27 Aug 2024 12:18:48 +0200 Subject: [PATCH 112/141] Increased timeout on a few tests as they would infrequently fail from delay of test context. --- tests/tango/test_tango_signals.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index 4fcda01833..8b9f11a036 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -457,7 +457,7 @@ async def test_tango_signal_r( proxy = await DeviceProxy(echo_device) if use_proxy else None py_type = py_type if use_dtype else None - timeout = 0.1 + timeout = 0.2 signal = tango_signal_r( datatype=py_type, read_trl=source, @@ -519,7 +519,7 @@ async def test_tango_signal_w( proxy = await DeviceProxy(echo_device) if use_proxy else None py_type = py_type if use_dtype else None - timeout = 0.1 + timeout = 0.2 signal = tango_signal_w( datatype=py_type, write_trl=source, @@ -594,7 +594,7 @@ async def test_tango_signal_rw( proxy = await DeviceProxy(echo_device) if use_proxy else None py_type = py_type if use_dtype else None - timeout = 0.1 + timeout = 0.2 signal = tango_signal_rw( datatype=py_type, read_trl=source, @@ -617,7 +617,7 @@ async def test_tango_signal_rw( @pytest.mark.parametrize("use_proxy", [True, False]) async def test_tango_signal_x(tango_test_device: str, use_proxy: bool): proxy = await DeviceProxy(tango_test_device) if use_proxy else None - timeout = 0.1 + timeout = 0.2 signal = tango_signal_x( write_trl=tango_test_device + "/" + "clear", device_proxy=proxy, @@ -677,7 +677,7 @@ async def test_tango_signal_auto_attrs( await prepare_device(echo_device, pv, initial_value) source = echo_device + "/" + pv proxy = await DeviceProxy(echo_device) if use_proxy else None - timeout = 0.1 + timeout = 0.2 async def _test_signal(dtype, proxy): signal = tango_signal_auto( @@ -751,7 +751,7 @@ async def test_tango_signal_auto_cmds( use_proxy: bool, ): source = echo_device + "/" + pv - timeout = 0.1 + timeout = 0.2 async def _test_signal(dtype, proxy): signal = tango_signal_auto( From e301cb5e7e6a34c887eedfd8989fdc19901c35e6 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 27 Aug 2024 16:00:48 +0200 Subject: [PATCH 113/141] Simplified how children are created from annotations --- .../tango/base_devices/_base_device.py | 37 ++++++------------- src/ophyd_async/tango/demo/_counter.py | 2 +- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index 66f990de5e..517d2fcf28 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -1,15 +1,11 @@ from __future__ import annotations -from typing import Optional, Union +from typing import Optional, Union, get_type_hints from ophyd_async.core import ( DEFAULT_TIMEOUT, Device, Signal, - SignalR, - SignalRW, - SignalW, - SignalX, ) from ophyd_async.tango.signal import ( infer_python_type, @@ -79,7 +75,8 @@ async def closure(): await super().connect(mock=mock, timeout=timeout) def register_signals(self): - for name, obj_type in self.__annotations__.items(): + annots = get_type_hints(self.__class__) + for name, obj_type in annots.items(): if hasattr(self, name): signal = getattr(self, name) if issubclass(type(signal), Signal): @@ -98,29 +95,17 @@ def register_signals(self): signal._backend = backend # noqa: SLF001 def create_children_from_annotations(self): - for attr_name, obj_type in self.__annotations__.items(): - if ( - isinstance(obj_type, type) - and issubclass(obj_type, Signal) - or obj_type is None - ): - if obj_type is SignalRW: - setattr(self, attr_name, SignalRW()) - elif obj_type is SignalR: - setattr(self, attr_name, SignalR()) - elif obj_type is SignalW: - setattr(self, attr_name, SignalW()) - elif obj_type is SignalX: - setattr(self, attr_name, SignalX()) - elif obj_type is Signal or obj_type is None: + annots = get_type_hints(self.__class__) + for attr_name, obj_type in annots.items(): + if isinstance(obj_type, type): + if obj_type is Signal: tango_name = attr_name.lstrip("_") + trl = f"{self.trl}/{tango_name}" setattr( - self, - attr_name, - infer_signal_frontend(trl=f"{self.trl}/" f"{tango_name}"), + self, attr_name, infer_signal_frontend(trl=trl, name=attr_name) ) - else: - raise ValueError(f"Invalid signal type {obj_type}") + elif issubclass(obj_type, Signal): + setattr(self, attr_name, obj_type(name=attr_name)) # -------------------------------------------------------------------- diff --git a/src/ophyd_async/tango/demo/_counter.py b/src/ophyd_async/tango/demo/_counter.py index d8387351ce..b6d7b9efa4 100644 --- a/src/ophyd_async/tango/demo/_counter.py +++ b/src/ophyd_async/tango/demo/_counter.py @@ -20,7 +20,7 @@ class TangoCounterConfig: class TangoCounter(TangoReadable): # Enter the name and type of the signals you want to use # If type is None or Signal, the type will be inferred from the Tango device - counts: None + counts: Signal sample_time: Signal state: Signal reset: Signal From c0e2079ca89b58a6fede35858fd92aec25ddaea1 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 2 Sep 2024 15:32:18 +0200 Subject: [PATCH 114/141] Remove method separators. --- .../tango/base_devices/_base_device.py | 1 - .../tango/base_devices/_tango_readable.py | 1 - src/ophyd_async/tango/signal/_signal.py | 7 --- .../tango/signal/_tango_transport.py | 49 ------------------- 4 files changed, 58 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index 517d2fcf28..03d8c9f342 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -108,7 +108,6 @@ def create_children_from_annotations(self): setattr(self, attr_name, obj_type(name=attr_name)) -# -------------------------------------------------------------------- def tango_polling(*args): """ Class decorator to set polling for Tango devices. This is useful for device servers diff --git a/src/ophyd_async/tango/base_devices/_tango_readable.py b/src/ophyd_async/tango/base_devices/_tango_readable.py index a168618717..08b52f3a1b 100644 --- a/src/ophyd_async/tango/base_devices/_tango_readable.py +++ b/src/ophyd_async/tango/base_devices/_tango_readable.py @@ -24,7 +24,6 @@ class TangoReadable(TangoDevice, StandardReadable): device is connected. """ - # -------------------------------------------------------------------- _polling: Tuple = (False, 0.1, None, 0.1) def __init__( diff --git a/src/ophyd_async/tango/signal/_signal.py b/src/ophyd_async/tango/signal/_signal.py index 2f5d4f259c..3febeb164b 100644 --- a/src/ophyd_async/tango/signal/_signal.py +++ b/src/ophyd_async/tango/signal/_signal.py @@ -26,7 +26,6 @@ def make_backend( return TangoSignalBackend(datatype, read_trl, write_trl, device_proxy) -# -------------------------------------------------------------------- def tango_signal_rw( datatype: Optional[Type[T]] = None, *, @@ -59,7 +58,6 @@ def tango_signal_rw( return SignalRW(backend, timeout=timeout, name=name) -# -------------------------------------------------------------------- def tango_signal_r( datatype: Optional[Type[T]] = None, *, @@ -89,7 +87,6 @@ def tango_signal_r( return SignalR(backend, timeout=timeout, name=name) -# -------------------------------------------------------------------- def tango_signal_w( datatype: Optional[Type[T]] = None, *, @@ -119,7 +116,6 @@ def tango_signal_w( return SignalW(backend, timeout=timeout, name=name) -# -------------------------------------------------------------------- def tango_signal_x( write_trl: str, device_proxy: Optional[DeviceProxy] = None, @@ -143,7 +139,6 @@ def tango_signal_x( return SignalX(backend, timeout=timeout, name=name) -# -------------------------------------------------------------------- def tango_signal_auto( datatype: Optional[Type[T]] = None, *, @@ -161,7 +156,6 @@ def tango_signal_auto( return signal -# -------------------------------------------------------------------- def infer_python_type(trl: str) -> Type[T]: device_trl, tr_name = trl.rsplit("/", 1) syn_proxy = SyncDeviceProxy(device_trl) @@ -186,7 +180,6 @@ def infer_python_type(trl: str) -> Type[T]: return npt.NDArray[py_type] if isarray else py_type -# -------------------------------------------------------------------- def infer_signal_frontend(trl, name: str = "", timeout: float = DEFAULT_TIMEOUT): device_trl, tr_name = trl.rsplit("/", 1) proxy = SyncDeviceProxy(device_trl) diff --git a/src/ophyd_async/tango/signal/_tango_transport.py b/src/ophyd_async/tango/signal/_tango_transport.py index dea54c8fb9..dd1126f610 100644 --- a/src/ophyd_async/tango/signal/_tango_transport.py +++ b/src/ophyd_async/tango/signal/_tango_transport.py @@ -41,7 +41,6 @@ A_BIT = 1e-5 -# -------------------------------------------------------------------- def ensure_proper_executor(func): @functools.wraps(func) async def wrapper(self, *args, **kwargs): @@ -53,7 +52,6 @@ async def wrapper(self, *args, **kwargs): return wrapper -# -------------------------------------------------------------------- def get_python_type(tango_type) -> tuple[bool, T, str]: array = is_array(tango_type) if is_int(tango_type, True): @@ -77,7 +75,6 @@ def get_python_type(tango_type) -> tuple[bool, T, str]: raise TypeError("Unknown TangoType") -# -------------------------------------------------------------------- class TangoProxy: support_events = True @@ -85,53 +82,43 @@ def __init__(self, device_proxy: DeviceProxy, name: str): self._proxy = device_proxy self._name = name - # -------------------------------------------------------------------- async def connect(self): """perform actions after proxy is connected, e.g. checks if signal can be subscribed""" - # -------------------------------------------------------------------- @abstractmethod async def get(self) -> T: """Get value from TRL""" - # -------------------------------------------------------------------- @abstractmethod async def get_w_value(self) -> T: """Get last written value from TRL""" - # -------------------------------------------------------------------- @abstractmethod async def put( self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None ) -> None: """Put value to TRL""" - # -------------------------------------------------------------------- @abstractmethod async def get_config(self) -> Union[AttributeInfoEx, CommandInfo]: """Get TRL config async""" - # -------------------------------------------------------------------- @abstractmethod async def get_reading(self) -> Reading: """Get reading from TRL""" - # -------------------------------------------------------------------- def has_subscription(self) -> bool: """indicates, that this trl already subscribed""" - # -------------------------------------------------------------------- @abstractmethod def subscribe_callback(self, callback: Optional[ReadingValueCallback]): """subscribe tango CHANGE event to callback""" - # -------------------------------------------------------------------- @abstractmethod def unsubscribe_callback(self): """delete CHANGE event subscription""" - # -------------------------------------------------------------------- @abstractmethod def set_polling( self, @@ -143,7 +130,6 @@ def set_polling( """Set polling parameters""" -# -------------------------------------------------------------------- class AttributeProxy(TangoProxy): _callback = None support_events = True @@ -156,7 +142,6 @@ class AttributeProxy(TangoProxy): exception = None _last_reading = {"value": None, "timestamp": 0, "alarm_severity": 0} - # -------------------------------------------------------------------- async def connect(self) -> None: try: eid = await self._proxy.subscribe_event( @@ -167,19 +152,16 @@ async def connect(self) -> None: except Exception: pass - # -------------------------------------------------------------------- @ensure_proper_executor async def get(self) -> T: attr = await self._proxy.read_attribute(self._name) return attr.value - # -------------------------------------------------------------------- @ensure_proper_executor async def get_w_value(self) -> T: attr = await self._proxy.read_attribute(self._name) return attr.w_value - # -------------------------------------------------------------------- @ensure_proper_executor async def put( self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None @@ -220,12 +202,10 @@ async def wait_for_reply(rd, to): return AsyncStatus(wait_for_reply(rid, timeout)) - # -------------------------------------------------------------------- @ensure_proper_executor async def get_config(self) -> AttributeInfoEx: return await self._proxy.get_attribute_config(self._name) - # -------------------------------------------------------------------- @ensure_proper_executor async def get_reading(self) -> Reading: attr = await self._proxy.read_attribute(self._name) @@ -235,11 +215,9 @@ async def get_reading(self) -> Reading: self._last_reading = reading return reading - # -------------------------------------------------------------------- def has_subscription(self) -> bool: return bool(self._callback) - # -------------------------------------------------------------------- def subscribe_callback(self, callback: Optional[ReadingValueCallback]): # If the attribute supports events, then we can subscribe to them # If the callback is not a callable, then we raise an error @@ -277,7 +255,6 @@ async def _poll(): " for which polling is disabled." ) - # -------------------------------------------------------------------- def unsubscribe_callback(self): if self._eid: self._proxy.unsubscribe_event(self._eid, green_mode=False) @@ -293,7 +270,6 @@ def unsubscribe_callback(self): pass self._callback = None - # -------------------------------------------------------------------- def _event_processor(self, event): if not event.err: value = event.attr_value.value @@ -304,7 +280,6 @@ def _event_processor(self, event): } self._callback(reading, value) - # -------------------------------------------------------------------- async def poll(self): """ Poll the attribute and call the callback if the value has changed by more @@ -377,7 +352,6 @@ async def poll(self): except Exception as e: raise RuntimeError(f"Could not poll the attribute: {e}") - # -------------------------------------------------------------------- def set_polling( self, allow_polling: bool = False, @@ -394,20 +368,16 @@ def set_polling( self._rel_change = rel_change -# -------------------------------------------------------------------- class CommandProxy(TangoProxy): support_events = True _last_reading = {"value": None, "timestamp": 0, "alarm_severity": 0} - # -------------------------------------------------------------------- async def get(self) -> T: return self._last_reading["value"] - # -------------------------------------------------------------------- async def get_w_value(self) -> T: return self._last_reading["value"] - # -------------------------------------------------------------------- @ensure_proper_executor async def put( self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None @@ -459,12 +429,10 @@ async def wait_for_reply(rd, to): return AsyncStatus(wait_for_reply(rid, timeout)) - # -------------------------------------------------------------------- @ensure_proper_executor async def get_config(self) -> CommandInfo: return await self._proxy.get_command_config(self._name) - # -------------------------------------------------------------------- async def get_reading(self) -> Reading: reading = Reading( value=self._last_reading["value"], @@ -473,7 +441,6 @@ async def get_reading(self) -> Reading: ) return reading - # -------------------------------------------------------------------- def set_polling( self, allow_polling: bool = False, @@ -484,7 +451,6 @@ def set_polling( pass -# -------------------------------------------------------------------- def get_dtype_extended(datatype): # DevState tango type does not have numpy equivalents dtype = get_dtype(datatype) @@ -494,7 +460,6 @@ def get_dtype_extended(datatype): return dtype -# -------------------------------------------------------------------- def get_trl_descriptor( datatype: Optional[Type], tango_resource: str, @@ -584,7 +549,6 @@ def get_trl_descriptor( return {"source": tango_resource, "dtype": tr_dtype_desc, "shape": []} -# -------------------------------------------------------------------- async def get_tango_trl( full_trl: str, device_proxy: Optional[DeviceProxy] ) -> TangoProxy: @@ -612,7 +576,6 @@ async def get_tango_trl( raise RuntimeError(f"{trl_name} cannot be found in {device_proxy.name()}") -# -------------------------------------------------------------------- class TangoSignalBackend(SignalBackend[T]): def __init__( self, @@ -634,11 +597,9 @@ def __init__( self.support_events = True self.status = None - # -------------------------------------------------------------------- def source(self, name: str) -> str: return self.read_trl - # -------------------------------------------------------------------- async def _connect_and_store_config(self, trl): try: self.proxies[trl] = await get_tango_trl(trl, self.proxies[trl]) @@ -648,7 +609,6 @@ async def _connect_and_store_config(self, trl): except CancelledError: raise NotConnected(f"Could not connect to {trl}") - # -------------------------------------------------------------------- async def connect(self, timeout: float = DEFAULT_TIMEOUT): if self.read_trl != self.write_trl: # Different, need to connect both @@ -664,29 +624,23 @@ async def connect(self, timeout: float = DEFAULT_TIMEOUT): self.datatype, self.read_trl, self.trl_configs ) - # -------------------------------------------------------------------- async def put(self, value: Optional[T], wait=True, timeout=None): self.status = None put_status = await self.proxies[self.write_trl].put(value, wait, timeout) self.status = put_status - # -------------------------------------------------------------------- async def get_datakey(self, source: str) -> DataKey: return self.descriptor - # -------------------------------------------------------------------- async def get_reading(self) -> Reading: return await self.proxies[self.read_trl].get_reading() - # -------------------------------------------------------------------- async def get_value(self) -> T: return await self.proxies[self.write_trl].get() - # -------------------------------------------------------------------- async def get_setpoint(self) -> T: return await self.proxies[self.write_trl].get_w_value() - # -------------------------------------------------------------------- def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: if self.support_events is False and self.polling[0] is False: raise RuntimeError( @@ -707,8 +661,6 @@ def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: else: self.proxies[self.read_trl].unsubscribe_callback() - # -------------------------------------------------------------------- - def set_polling( self, allow_polling: bool = True, @@ -718,6 +670,5 @@ def set_polling( ): self.polling = (allow_polling, polling_period, abs_change, rel_change) - # -------------------------------------------------------------------- def allow_events(self, allow: Optional[bool] = True): self.support_events = allow From 063bf7ae6a5831123ebc498a9d0d53f3872c1f92 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 5 Sep 2024 13:52:22 +0200 Subject: [PATCH 115/141] Updated pyproject.toml so that pytango can be installed with pip install ophyd-async[tango] --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cf732d03e6..23b1471aa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,10 +33,12 @@ requires-python = ">=3.10" ca = ["aioca>=1.6"] pva = ["p4p"] sim = ["h5py"] +tango = ["pytango>=10.0.0rc1"] dev = [ "ophyd_async[pva]", "ophyd_async[sim]", "ophyd_async[ca]", + "ophyd_async[tango]", "black", "flake8", "flake8-isort", @@ -54,7 +56,6 @@ dev = [ "pydata-sphinx-theme>=0.12", "pyepics>=3.4.2", "pyside6==6.7.0", - "pytango>=10.0.0rc1", "pytest", "pytest-asyncio", "pytest-cov", From 3a91d7711554223690a93d80eeb1676aa4b4c9b8 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 5 Sep 2024 14:55:48 +0200 Subject: [PATCH 116/141] tango_polling will take a tuple to enable polling across all signals in a device and/or a dict to enable polling for individual signals. --- src/ophyd_async/tango/base_devices/_tango_readable.py | 4 +--- src/ophyd_async/tango/demo/_counter.py | 5 ++++- src/ophyd_async/tango/demo/_mover.py | 3 ++- src/ophyd_async/tango/signal/_tango_transport.py | 8 ++++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/_tango_readable.py b/src/ophyd_async/tango/base_devices/_tango_readable.py index 08b52f3a1b..94e85d96a3 100644 --- a/src/ophyd_async/tango/base_devices/_tango_readable.py +++ b/src/ophyd_async/tango/base_devices/_tango_readable.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional, Tuple, Union +from typing import Optional, Union from ophyd_async.core import ( StandardReadable, @@ -24,8 +24,6 @@ class TangoReadable(TangoDevice, StandardReadable): device is connected. """ - _polling: Tuple = (False, 0.1, None, 0.1) - def __init__( self, trl: Optional[str] = None, diff --git a/src/ophyd_async/tango/demo/_counter.py b/src/ophyd_async/tango/demo/_counter.py index b6d7b9efa4..6280358a47 100644 --- a/src/ophyd_async/tango/demo/_counter.py +++ b/src/ophyd_async/tango/demo/_counter.py @@ -9,7 +9,7 @@ Signal, SignalX, ) -from ophyd_async.tango import TangoReadable +from ophyd_async.tango import TangoReadable, tango_polling @dataclass @@ -17,6 +17,9 @@ class TangoCounterConfig: sample_time: Optional[float] = None +# Enable device level polling, useful for servers that do not support events +# Polling for individual signal can be enabled with a dict +@tango_polling((0.1, 0.1, 0.1), {"counts": (1.0, 0.1, 0.1)}) class TangoCounter(TangoReadable): # Enter the name and type of the signals you want to use # If type is None or Signal, the type will be inferred from the Tango device diff --git a/src/ophyd_async/tango/demo/_mover.py b/src/ophyd_async/tango/demo/_mover.py index c7d1c6f0df..847f268ed3 100644 --- a/src/ophyd_async/tango/demo/_mover.py +++ b/src/ophyd_async/tango/demo/_mover.py @@ -27,7 +27,8 @@ class TangoMoverConfig: velocity: Optional[float] = None -@tango_polling(0.1, 0.1, 0.1) +# Enable device level polling, useful for servers that do not support events +@tango_polling((0.1, 0.1, 0.1)) class TangoMover(TangoReadable, Movable, Stoppable): # Enter the name and type of the signals you want to use # If type is None or Signal, the type will be inferred from the Tango device diff --git a/src/ophyd_async/tango/signal/_tango_transport.py b/src/ophyd_async/tango/signal/_tango_transport.py index dd1126f610..8d7742b5c8 100644 --- a/src/ophyd_async/tango/signal/_tango_transport.py +++ b/src/ophyd_async/tango/signal/_tango_transport.py @@ -593,7 +593,7 @@ def __init__( } self.trl_configs: Dict[str, AttributeInfoEx] = {} self.descriptor: Descriptor = {} # type: ignore - self.polling = (False, 0.1, None, 0.1) + self._polling = (False, 0.1, None, 0.1) self.support_events = True self.status = None @@ -619,7 +619,7 @@ async def connect(self, timeout: float = DEFAULT_TIMEOUT): else: # The same, so only need to connect one await self._connect_and_store_config(self.read_trl) - self.proxies[self.read_trl].set_polling(*self.polling) + self.proxies[self.read_trl].set_polling(*self._polling) self.descriptor = get_trl_descriptor( self.datatype, self.read_trl, self.trl_configs ) @@ -642,7 +642,7 @@ async def get_setpoint(self) -> T: return await self.proxies[self.write_trl].get_w_value() def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: - if self.support_events is False and self.polling[0] is False: + if self.support_events is False and self._polling[0] is False: raise RuntimeError( f"Cannot set event for {self.read_trl}. " "Cannot set a callback on an attribute that does not support events and" @@ -668,7 +668,7 @@ def set_polling( abs_change=None, rel_change=0.1, ): - self.polling = (allow_polling, polling_period, abs_change, rel_change) + self._polling = (allow_polling, polling_period, abs_change, rel_change) def allow_events(self, allow: Optional[bool] = True): self.support_events = allow From d59fb5feed37937e9796ba9eb587e0bcd3bf2215 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 5 Sep 2024 16:38:55 +0200 Subject: [PATCH 117/141] Fixed some tests. --- src/ophyd_async/tango/demo/_mover.py | 2 +- tests/tango/test_base_device.py | 1 + tests/tango/test_tango_transport.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ophyd_async/tango/demo/_mover.py b/src/ophyd_async/tango/demo/_mover.py index 847f268ed3..f491a31912 100644 --- a/src/ophyd_async/tango/demo/_mover.py +++ b/src/ophyd_async/tango/demo/_mover.py @@ -77,7 +77,7 @@ async def set(self, value: float, timeout: CalculatableTimeout = CalculateTimeou async def _wait_for_idle(self): if self.state._backend.support_events is False: # noqa: SLF001 - if self.state._backend.polling[0] is False: # noqa: SLF001 + if self.state._backend._polling[0] is False: # noqa: SLF001 raise RuntimeError("State does not support events or polling") event = asyncio.Event() diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 9e947732d0..a27ebba114 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -386,6 +386,7 @@ async def test_tango_demo(demo_test_context): await motor1.connect() await counter1.connect() await counter2.connect() + print("Polling:", motor1._polling) RE = RunEngine() diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index 2a7c80e2ab..b21c041bd5 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -694,7 +694,7 @@ def callback(reading, value): @pytest.mark.asyncio async def test_tango_transport_set_polling(transport): transport.set_polling(True, 0.1, 1, 0.1) - assert transport.polling == (True, 0.1, 1, 0.1) + assert transport._polling == (True, 0.1, 1, 0.1) # -------------------------------------------------------------------- From a1a041035b7759229fcc79013e0aa8f446207c40 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 5 Sep 2024 17:28:58 +0200 Subject: [PATCH 118/141] Moved tango_demo to docs. --- docs/examples/tango_demo.py | 53 ++++++++++++++++++++++++++++ src/ophyd_async/tango/demo/README.md | 51 -------------------------- 2 files changed, 53 insertions(+), 51 deletions(-) create mode 100644 docs/examples/tango_demo.py delete mode 100644 src/ophyd_async/tango/demo/README.md diff --git a/docs/examples/tango_demo.py b/docs/examples/tango_demo.py new file mode 100644 index 0000000000..3a978fde04 --- /dev/null +++ b/docs/examples/tango_demo.py @@ -0,0 +1,53 @@ +import asyncio + +import bluesky.plans as bp +from bluesky import RunEngine +from bluesky.callbacks.best_effort import BestEffortCallback +from bluesky.utils import ProgressBarManager + +from ophyd_async.tango.demo import ( + DemoCounter, + DemoMover, + TangoCounter, + TangoMover, +) +from tango.test_context import MultiDeviceTestContext + +content = ( + { + "class": DemoMover, + "devices": [{"name": "demo/motor/1"}], + }, + { + "class": DemoCounter, + "devices": [{"name": "demo/counter/1"}, {"name": "demo/counter/2"}], + }, +) + +tango_context = MultiDeviceTestContext(content) + + +async def main(): + with tango_context as context: + motor1 = TangoMover( + trl=context.get_device_access("demo/motor/1"), name="motor1" + ) + counter1 = TangoCounter( + trl=context.get_device_access("demo/counter/1"), name="counter1" + ) + counter2 = TangoCounter( + trl=context.get_device_access("demo/counter/2"), name="counter2" + ) + await motor1.connect() + await counter1.connect() + await counter2.connect() + + RE = RunEngine() + RE.subscribe(BestEffortCallback()) + RE.waiting_hook = ProgressBarManager() + # RE(bps.mv(motor1, 1)) + RE(bp.scan([counter1, counter2], motor1, -1, 1, 10)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/ophyd_async/tango/demo/README.md b/src/ophyd_async/tango/demo/README.md deleted file mode 100644 index af1fa6071d..0000000000 --- a/src/ophyd_async/tango/demo/README.md +++ /dev/null @@ -1,51 +0,0 @@ -The following is intended to be a demonstration of Tango support for ophyd-async. -The usage of the Tango control system without a real Tango server is limited and -intended to be used in tests. All operations using the demo devices must be performed -within the MultiDeviceTestContext context as demonstrated here. - -```python -from tango.test_context import MultiDeviceTestContext -from ophyd_async.tango.demo import ( - DemoMover, - TangoMover, - DemoCounter, - TangoCounter, -) - -from bluesky import RunEngine -import bluesky.plans as bp -from bluesky.callbacks.best_effort import BestEffortCallback -from bluesky.utils import ProgressBarManager - -content = ( - { - "class": DemoMover, - "devices": [ - {"name": "demo/motor/1"} - ], - }, - { - "class": DemoCounter, - "devices": [ - {"name": "demo/counter/1"}, - {"name": "demo/counter/2"} - ], - } -) - -tango_context = MultiDeviceTestContext(content) - -with tango_context as context: - motor1 = TangoMover(trl=context.get_device_access("demo/motor/1"), name="motor1") - counter1 = TangoCounter(trl=context.get_device_access("demo/counter/1"), name="counter1") - counter2 = TangoCounter(trl=context.get_device_access("demo/counter/2"), name="counter2") - await motor1.connect() - await counter1.connect() - await counter2.connect() - - RE = RunEngine() - RE.subscribe(BestEffortCallback()) - RE.waiting_hook = ProgressBarManager() - #RE(bps.mv(motor1, 1)) - RE(bp.scan([counter1, counter2], motor1, -1, 1, 10)) -``` \ No newline at end of file From d18607e843ab0fe1529f1b4caf4b83db64fada18 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 6 Sep 2024 08:53:22 +0200 Subject: [PATCH 119/141] Somehow the polling changes didn't make it into a previous commit --- .../tango/base_devices/_base_device.py | 63 +++++++++++++++++-- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index 03d8c9f342..8c28f2530e 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional, Union, get_type_hints +from typing import Dict, Optional, Tuple, Union, get_type_hints from ophyd_async.core import ( DEFAULT_TIMEOUT, @@ -33,7 +33,8 @@ class TangoDevice(Device): trl: str = "" proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] = None - _polling: tuple = (False, 0.1, None, 0.1) + _polling: Tuple[bool, float, float, float] = (False, 0.1, None, 0.1) + _signal_polling: Dict[str, Tuple[bool, float, float, float]] = {} def __init__( self, @@ -92,6 +93,9 @@ def register_signals(self): if self._polling[0]: backend.allow_events(False) backend.set_polling(*self._polling) + if name in self._signal_polling: + backend.allow_events(False) + backend.set_polling(*self._signal_polling[name]) signal._backend = backend # noqa: SLF001 def create_children_from_annotations(self): @@ -108,14 +112,61 @@ def create_children_from_annotations(self): setattr(self, attr_name, obj_type(name=attr_name)) -def tango_polling(*args): +def tango_polling( + polling: Optional[ + Union[Tuple[float, float, float], Dict[str, Tuple[float, float, float]]] + ] = None, + signal_polling: Optional[Dict[str, Tuple[float, float, float]]] = None, +): """ - Class decorator to set polling for Tango devices. This is useful for device servers - that do not support event-driven updates. + Class decorator to configure polling for Tango devices. + + This decorator allows for the configuration of both device-level and signal-level + polling for Tango devices. Polling is useful for device servers that do not support + event-driven updates. + + Parameters + ---------- + polling : Optional[Union[Tuple[float, float, float], + Dict[str, Tuple[float, float, float]]]], optional + Device-level polling configuration as a tuple of three floats representing the + polling interval, polling timeout, and polling delay. Alternatively, + a dictionary can be provided to specify signal-level polling configurations + directly. + signal_polling : Optional[Dict[str, Tuple[float, float, float]]], optional + Signal-level polling configuration as a dictionary where keys are signal names + and values are tuples of three floats representing the polling interval, polling + timeout, and polling delay. + + Returns + ------- + Callable + A class decorator that sets the `_polling` and `_signal_polling` attributes on + the decorated class. + + Example + ------- + Device-level and signal-level polling: + @tango_polling( + polling=(0.5, 1.0, 0.1), + signal_polling={ + 'signal1': (0.5, 1.0, 0.1), + 'signal2': (1.0, 2.0, 0.2), + } + ) + class MyTangoDevice(TangoDevice): + signal1: Signal + signal2: Signal """ + if isinstance(polling, dict): + signal_polling = polling + polling = None def decorator(cls): - cls._polling = (True, *args) + if polling is not None: + cls._polling = (True, *polling) + if signal_polling is not None: + cls._signal_polling = {k: (True, *v) for k, v in signal_polling.items()} return cls return decorator From f66d403040e1d9c90502e0f5dcb866711729ac2d Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 12 Sep 2024 13:42:40 +0200 Subject: [PATCH 120/141] When creating a tango device, the signal type is now required in type annotations unless it is a SignalX. Annotations may also include Devices or DeviceVectors. The kwargs for these objects should be passed to init for the device as a dict called . Signals which are not explicitly annotated are automatically filled in as attributes when the device is connected. Their signal read/write character and datatype is inferred from the tango server. --- src/ophyd_async/tango/__init__.py | 4 +- .../tango/base_devices/_base_device.py | 164 +++++++++++++----- src/ophyd_async/tango/demo/_counter.py | 11 +- src/ophyd_async/tango/demo/_mover.py | 16 +- src/ophyd_async/tango/signal/__init__.py | 4 +- src/ophyd_async/tango/signal/_signal.py | 97 ++++++----- tests/tango/test_base_device.py | 46 ++--- tests/tango/test_tango_signals.py | 67 ++----- 8 files changed, 229 insertions(+), 180 deletions(-) diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py index 3f8ef2d0d9..371a590ab5 100644 --- a/src/ophyd_async/tango/__init__.py +++ b/src/ophyd_async/tango/__init__.py @@ -13,7 +13,7 @@ get_tango_trl, get_trl_descriptor, infer_python_type, - infer_signal_frontend, + infer_signal_character, make_backend, tango_signal_auto, tango_signal_r, @@ -32,7 +32,7 @@ get_trl_descriptor, get_tango_trl, infer_python_type, - infer_signal_frontend, + infer_signal_character, make_backend, AttributeProxy, CommandProxy, diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index 8c28f2530e..3e03108597 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -1,20 +1,32 @@ from __future__ import annotations -from typing import Dict, Optional, Tuple, Union, get_type_hints +from typing import ( + Dict, + Optional, + Tuple, + Type, + TypeVar, + Union, + get_args, + get_origin, + get_type_hints, +) from ophyd_async.core import ( DEFAULT_TIMEOUT, Device, + DeviceVector, Signal, ) from ophyd_async.tango.signal import ( - infer_python_type, - infer_signal_frontend, make_backend, + tango_signal_auto, ) from tango import DeviceProxy as SyncDeviceProxy from tango.asyncio import DeviceProxy as AsyncDeviceProxy +T = TypeVar("T") + class TangoDevice(Device): """ @@ -35,6 +47,7 @@ class TangoDevice(Device): proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] = None _polling: Tuple[bool, float, float, float] = (False, 0.1, None, 0.1) _signal_polling: Dict[str, Tuple[bool, float, float, float]] = {} + _poll_only_annotated_signals: bool = True def __init__( self, @@ -47,8 +60,7 @@ def __init__( self.trl = trl if trl else "" self.proxy = device_proxy - - self.create_children_from_annotations() + tango_create_children_from_annotations(self) super().__init__(name=name) async def connect( @@ -70,46 +82,29 @@ async def closure(): await closure() self.register_signals() + await fill_proxy_entries(self) + # set_name should be called again to propagate the new signal names self.set_name(self.name) + # Set the polling configuration + if self._polling[0]: + for child in self.children(): + if issubclass(type(child[1]), Signal): + child[1]._backend.set_polling(*self._polling) # noqa: SLF001 + child[1]._backend.allow_events(False) # noqa: SLF001 + if self._signal_polling: + for signal_name, polling in self._signal_polling.items(): + if hasattr(self, signal_name): + attr = getattr(self, signal_name) + attr._backend.set_polling(*polling) # noqa: SLF001 + attr._backend.allow_events(False) # noqa: SLF001 + await super().connect(mock=mock, timeout=timeout) + # Users can override this method to register new signals def register_signals(self): - annots = get_type_hints(self.__class__) - for name, obj_type in annots.items(): - if hasattr(self, name): - signal = getattr(self, name) - if issubclass(type(signal), Signal): - tango_name = name.lstrip("_") - read_trl = f"{self.trl}/{tango_name}" - datatype = infer_python_type(read_trl) - backend = make_backend( - datatype=datatype, - read_trl=read_trl, - write_trl=read_trl, - device_proxy=self.proxy, - ) - if self._polling[0]: - backend.allow_events(False) - backend.set_polling(*self._polling) - if name in self._signal_polling: - backend.allow_events(False) - backend.set_polling(*self._signal_polling[name]) - signal._backend = backend # noqa: SLF001 - - def create_children_from_annotations(self): - annots = get_type_hints(self.__class__) - for attr_name, obj_type in annots.items(): - if isinstance(obj_type, type): - if obj_type is Signal: - tango_name = attr_name.lstrip("_") - trl = f"{self.trl}/{tango_name}" - setattr( - self, attr_name, infer_signal_frontend(trl=trl, name=attr_name) - ) - elif issubclass(obj_type, Signal): - setattr(self, attr_name, obj_type(name=attr_name)) + pass def tango_polling( @@ -170,3 +165,94 @@ def decorator(cls): return cls return decorator + + +def tango_create_children_from_annotations( + device: TangoDevice, + included_optional_fields: Tuple[str, ...] = (), + device_vectors: Optional[Dict[str, int]] = None, +): + """Initialize blocks at __init__ of `device`.""" + for name, device_type in get_type_hints(type(device)).items(): + if name in ("_name", "parent"): + continue + + device_type, is_optional = _strip_union(device_type) + if is_optional and name not in included_optional_fields: + continue + + is_device_vector, device_type = _strip_device_vector(device_type) + if is_device_vector: + kwargs = "_" + name + "_kwargs" + kwargs = getattr(device, kwargs, {}) + prefix = kwargs["prefix"] + count = kwargs["count"] + n_device_vector = DeviceVector( + {i: device_type(f"{prefix}{i}") for i in range(1, count + 1)} + ) + setattr(device, name, n_device_vector) + + else: + origin = get_origin(device_type) + origin = origin if origin else device_type + + if issubclass(origin, Signal): + datatype = None + tango_name = name.lstrip("_") + read_trl = f"{device.trl}/{tango_name}" + type_args = get_args(device_type) + if type_args: + datatype = type_args[0] + backend = make_backend( + datatype=datatype, + read_trl=read_trl, + write_trl=read_trl, + device_proxy=device.proxy, + ) + setattr(device, name, origin(name=name, backend=backend)) + + elif issubclass(origin, Device) or isinstance(origin, Device): + kwargs = "_" + name + "_kwargs" + kwargs = getattr(device, kwargs, "") + setattr(device, name, origin(**kwargs)) + + +async def fill_proxy_entries(device: TangoDevice): + proxy_trl = device.trl + children = [name.lstrip("_") for name, _ in device.children()] + proxy_attributes = list(device.proxy.get_attribute_list()) + proxy_commands = list(device.proxy.get_command_list()) + combined = proxy_attributes + proxy_commands + + for name in combined: + if name not in children: + full_trl = f"{proxy_trl}/{name}" + try: + auto_signal = await tango_signal_auto( + trl=full_trl, device_proxy=device.proxy + ) + setattr(device, name, auto_signal) + except RuntimeError as e: + if "Commands with different in and out dtypes" in str(e): + print( + f"Skipping {name}. Commands with different in and out dtypes" + f" are not supported." + ) + continue + raise e + + +def _strip_union(field: Union[Union[T], T]) -> Tuple[T, bool]: + if get_origin(field) is Union: + args = get_args(field) + is_optional = type(None) in args + for arg in args: + if arg is not type(None): + return arg, is_optional + return field, False + + +def _strip_device_vector(field: Union[Type[Device]]) -> Tuple[bool, Type[Device]]: + if get_origin(field) is DeviceVector: + return True, get_args(field)[0] + return False, field diff --git a/src/ophyd_async/tango/demo/_counter.py b/src/ophyd_async/tango/demo/_counter.py index 6280358a47..1a8ff7dc3d 100644 --- a/src/ophyd_async/tango/demo/_counter.py +++ b/src/ophyd_async/tango/demo/_counter.py @@ -6,7 +6,8 @@ AsyncStatus, ConfigSignal, HintedSignal, - Signal, + SignalR, + SignalRW, SignalX, ) from ophyd_async.tango import TangoReadable, tango_polling @@ -23,15 +24,13 @@ class TangoCounterConfig: class TangoCounter(TangoReadable): # Enter the name and type of the signals you want to use # If type is None or Signal, the type will be inferred from the Tango device - counts: Signal - sample_time: Signal - state: Signal - reset: Signal + counts: SignalR[int] + sample_time: SignalRW[float] start: SignalX def __init__(self, trl: str, name=""): super().__init__(trl, name=name) - self.add_readables([self.counts], HintedSignal.uncached) + self.add_readables([self.counts], HintedSignal) self.add_readables([self.sample_time], ConfigSignal) @AsyncStatus.wrap diff --git a/src/ophyd_async/tango/demo/_mover.py b/src/ophyd_async/tango/demo/_mover.py index f491a31912..bf54f6b014 100644 --- a/src/ophyd_async/tango/demo/_mover.py +++ b/src/ophyd_async/tango/demo/_mover.py @@ -11,7 +11,6 @@ CalculateTimeout, ConfigSignal, HintedSignal, - SignalR, SignalRW, SignalX, WatchableAsyncStatus, @@ -32,9 +31,8 @@ class TangoMoverConfig: class TangoMover(TangoReadable, Movable, Stoppable): # Enter the name and type of the signals you want to use # If type is None or Signal, the type will be inferred from the Tango device - position: SignalRW - velocity: SignalRW - state: SignalR + position: SignalRW[float] + velocity: SignalRW[float] _stop: SignalX def __init__(self, trl: str, name=""): @@ -57,7 +55,7 @@ async def set(self, value: float, timeout: CalculatableTimeout = CalculateTimeou await self.position.set(value, wait=False, timeout=timeout) # Wait for the motor to stop - move_status = AsyncStatus(self._wait_for_idle()) + move_status = self.wait_for_idle() try: async for current_position in observe_value( @@ -75,11 +73,8 @@ async def set(self, value: float, timeout: CalculatableTimeout = CalculateTimeou if not self._set_success: raise RuntimeError("Motor was stopped") - async def _wait_for_idle(self): - if self.state._backend.support_events is False: # noqa: SLF001 - if self.state._backend._polling[0] is False: # noqa: SLF001 - raise RuntimeError("State does not support events or polling") - + @AsyncStatus.wrap + async def wait_for_idle(self): event = asyncio.Event() def _wait(value: dict[str, Reading]): @@ -88,6 +83,7 @@ def _wait(value: dict[str, Reading]): self.state.subscribe(_wait) await event.wait() + self.state.clear_sub(_wait) def stop(self, success: bool = True) -> AsyncStatus: self._set_success = success diff --git a/src/ophyd_async/tango/signal/__init__.py b/src/ophyd_async/tango/signal/__init__.py index bf8abbcbba..a285e34c03 100644 --- a/src/ophyd_async/tango/signal/__init__.py +++ b/src/ophyd_async/tango/signal/__init__.py @@ -1,6 +1,6 @@ from ._signal import ( infer_python_type, - infer_signal_frontend, + infer_signal_character, make_backend, tango_signal_auto, tango_signal_r, @@ -29,7 +29,7 @@ "get_trl_descriptor", "get_tango_trl", "infer_python_type", - "infer_signal_frontend", + "infer_signal_character", "make_backend", "tango_signal_r", "tango_signal_rw", diff --git a/src/ophyd_async/tango/signal/_signal.py b/src/ophyd_async/tango/signal/_signal.py index 3febeb164b..2395e96eaf 100644 --- a/src/ophyd_async/tango/signal/_signal.py +++ b/src/ophyd_async/tango/signal/_signal.py @@ -13,7 +13,6 @@ get_python_type, ) from tango import AttrDataFormat, AttrWriteType, CmdArgType, DevState -from tango import DeviceProxy as SyncDeviceProxy from tango.asyncio import DeviceProxy @@ -27,8 +26,7 @@ def make_backend( def tango_signal_rw( - datatype: Optional[Type[T]] = None, - *, + datatype: Type[T], read_trl: str, write_trl: Optional[str] = None, device_proxy: Optional[DeviceProxy] = None, @@ -52,15 +50,12 @@ def tango_signal_rw( name: The name of the Signal """ - if datatype is None: - datatype = infer_python_type(read_trl) backend = make_backend(datatype, read_trl, write_trl or read_trl, device_proxy) return SignalRW(backend, timeout=timeout, name=name) def tango_signal_r( - datatype: Optional[Type[T]] = None, - *, + datatype: Type[T], read_trl: str, device_proxy: Optional[DeviceProxy] = None, timeout: float = DEFAULT_TIMEOUT, @@ -81,15 +76,12 @@ def tango_signal_r( name: The name of the Signal """ - if datatype is None: - datatype = infer_python_type(read_trl) backend = make_backend(datatype, read_trl, read_trl, device_proxy) return SignalR(backend, timeout=timeout, name=name) def tango_signal_w( - datatype: Optional[Type[T]] = None, - *, + datatype: Type[T], write_trl: str, device_proxy: Optional[DeviceProxy] = None, timeout: float = DEFAULT_TIMEOUT, @@ -110,8 +102,6 @@ def tango_signal_w( name: The name of the Signal """ - if datatype is None: - datatype = infer_python_type(write_trl) backend = make_backend(datatype, write_trl, write_trl, device_proxy) return SignalW(backend, timeout=timeout, name=name) @@ -139,32 +129,48 @@ def tango_signal_x( return SignalX(backend, timeout=timeout, name=name) -def tango_signal_auto( +async def tango_signal_auto( datatype: Optional[Type[T]] = None, *, trl: str, - device_proxy: Optional[DeviceProxy] = None, + device_proxy: Optional[DeviceProxy], timeout: float = DEFAULT_TIMEOUT, name: str = "", -) -> Union[SignalW, SignalX, SignalR, SignalRW]: +) -> Union[SignalW, SignalX, SignalR, SignalRW, None]: + try: + signal_character = await infer_signal_character(trl, device_proxy) + except RuntimeError as e: + if "Commands with different in and out dtypes" in str(e): + return None + else: + raise e + if datatype is None: - datatype = infer_python_type(trl) - backend = make_backend(datatype, trl, trl, device_proxy) - signal = infer_signal_frontend(trl, name, timeout) - signal._backend = backend # noqa: SLF001 + datatype = await infer_python_type(trl, device_proxy) - return signal + backend = make_backend(datatype, trl, trl, device_proxy) + if signal_character == "RW": + return SignalRW(backend=backend, timeout=timeout, name=name) + if signal_character == "R": + return SignalR(backend=backend, timeout=timeout, name=name) + if signal_character == "W": + return SignalW(backend=backend, timeout=timeout, name=name) + if signal_character == "X": + return SignalX(backend=backend, timeout=timeout, name=name) -def infer_python_type(trl: str) -> Type[T]: +async def infer_python_type(trl: str = "", proxy: DeviceProxy = None) -> Type[T]: device_trl, tr_name = trl.rsplit("/", 1) - syn_proxy = SyncDeviceProxy(device_trl) + if proxy is None: + dev_proxy = await DeviceProxy(device_trl) + else: + dev_proxy = proxy - if tr_name in syn_proxy.get_command_list(): - config = syn_proxy.get_command_config(tr_name) + if tr_name in dev_proxy.get_command_list(): + config = await dev_proxy.get_command_config(tr_name) isarray, py_type, _ = get_python_type(config.in_type) - elif tr_name in syn_proxy.get_attribute_list(): - config = syn_proxy.get_attribute_config(tr_name) + elif tr_name in dev_proxy.get_attribute_list(): + config = await dev_proxy.get_attribute_config(tr_name) isarray, py_type, _ = get_python_type(config.data_type) if py_type is Enum: enum_dict = {label: i for i, label in enumerate(config.enum_labels)} @@ -180,29 +186,36 @@ def infer_python_type(trl: str) -> Type[T]: return npt.NDArray[py_type] if isarray else py_type -def infer_signal_frontend(trl, name: str = "", timeout: float = DEFAULT_TIMEOUT): +async def infer_signal_character(trl, proxy: DeviceProxy = None) -> str: device_trl, tr_name = trl.rsplit("/", 1) - proxy = SyncDeviceProxy(device_trl) + if proxy is None: + dev_proxy = await DeviceProxy(device_trl) + else: + dev_proxy = proxy - if tr_name in proxy.get_pipe_list(): + if tr_name in dev_proxy.get_pipe_list(): raise NotImplementedError("Pipes are not supported") - if tr_name not in proxy.get_attribute_list(): - if tr_name not in proxy.get_command_list(): + if tr_name not in dev_proxy.get_attribute_list(): + if tr_name not in dev_proxy.get_command_list(): raise RuntimeError(f"Cannot find {tr_name} in {device_trl}") - if tr_name in proxy.get_attribute_list(): - config = proxy.get_attribute_config(tr_name) + if tr_name in dev_proxy.get_attribute_list(): + config = await dev_proxy.get_attribute_config(tr_name) if config.writable in [AttrWriteType.READ_WRITE, AttrWriteType.READ_WITH_WRITE]: - return SignalRW(name=name, timeout=timeout) + return "RW" elif config.writable == AttrWriteType.READ: - return SignalR(name=name, timeout=timeout) + return "R" else: - return SignalW(name=name, timeout=timeout) + return "W" - if tr_name in proxy.get_command_list(): - config = proxy.get_command_config(tr_name) + if tr_name in dev_proxy.get_command_list(): + config = await dev_proxy.get_command_config(tr_name) if config.in_type == CmdArgType.DevVoid: - return SignalX(name=name, timeout=timeout) - elif config.out_type != CmdArgType.DevVoid: - return SignalRW(name=name, timeout=timeout) + return "X" + elif config.in_type != config.out_type: + raise RuntimeError( + "Commands with different in and out dtypes are not" " supported" + ) + else: + return "RW" diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index a27ebba114..d47d62e5e7 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -6,12 +6,13 @@ import bluesky.plan_stubs as bps import bluesky.plans as bp import numpy as np +import numpy.typing as npt import pytest from bluesky import RunEngine import tango -from ophyd_async.core import DeviceCollector, T -from ophyd_async.tango import TangoReadable, get_python_type, tango_signal_auto +from ophyd_async.core import DeviceCollector, HintedSignal, SignalRW, T +from ophyd_async.tango import TangoReadable, get_python_type from ophyd_async.tango.demo import ( DemoCounter, DemoMover, @@ -176,6 +177,9 @@ def raise_exception_cmd(self): # -------------------------------------------------------------------- class TestTangoReadable(TangoReadable): __test__ = False + justvalue: SignalRW[int] + array: SignalRW[npt.NDArray[float]] + limitedvalue: SignalRW[float] def __init__( self, @@ -183,20 +187,10 @@ def __init__( device_proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] = None, name: str = "", ) -> None: - self.trl = trl - TangoReadable.__init__(self, trl, device_proxy, name) - - def register_signals(self): - for feature in TESTED_FEATURES: - setattr( - self, - feature, - tango_signal_auto(datatype=None, trl=f"{self.trl}/{feature}"), - ) - attr = getattr(self, feature) - attr._backend.allow_events(False) - attr._backend.set_polling(0.1, 0.1, 0.1) - self.add_readables([attr]) + super().__init__(trl, device_proxy, name=name) + self.add_readables( + [self.justvalue, self.array, self.limitedvalue], HintedSignal.uncached + ) # -------------------------------------------------------------------- @@ -349,25 +343,20 @@ async def test_connect_proxy(tango_test_device, proxy: Optional[bool]): await test_device.connect() assert isinstance(test_device.proxy, tango._tango.DeviceProxy) else: - proxy = SyncDeviceProxy(tango_test_device) - test_device = TestTangoReadable(device_proxy=proxy) - await test_device.connect() - assert isinstance(test_device.proxy, tango._tango.DeviceProxy) + proxy = None + with pytest.raises(ValueError) as excinfo: + test_device = TestTangoReadable(device_proxy=proxy) + assert "Either 'trl' or 'device_proxy' must be provided." in str(excinfo.value) # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_with_bluesky(tango_test_device): - async def connect(): - async with DeviceCollector(): - device = TestTangoReadable(tango_test_device) - return device - - ophyd_dev = await connect() - # now let's do some bluesky stuff RE = RunEngine() - RE(bp.count([ophyd_dev], 1)) + with DeviceCollector(): + device = TestTangoReadable(tango_test_device) + RE(bp.count([device])) # -------------------------------------------------------------------- @@ -386,7 +375,6 @@ async def test_tango_demo(demo_test_context): await motor1.connect() await counter1.connect() await counter2.connect() - print("Polling:", motor1._polling) RE = RunEngine() diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index 8b9f11a036..136600b6d9 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -11,7 +11,7 @@ from bluesky.protocols import Reading from test_base_device import TestDevice -from ophyd_async.core import SignalBackend, SignalRW, SignalX, T +from ophyd_async.core import SignalBackend, SignalR, SignalRW, SignalW, SignalX, T from ophyd_async.tango import ( TangoSignalBackend, tango_signal_auto, @@ -411,7 +411,7 @@ async def test_backend_get_put_monitor_cmd( # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( - "pv, tango_type, d_format, py_type, initial_value, put_value, use_dtype, use_proxy", + "pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy", [ ( pv, @@ -420,7 +420,6 @@ async def test_backend_get_put_monitor_cmd( py_type, initial_value, put_value, - use_dtype, use_proxy, ) for ( @@ -431,15 +430,9 @@ async def test_backend_get_put_monitor_cmd( initial_value, put_value, ) in ATTRIBUTES_SET - for use_dtype in [True, False] - for use_proxy in [True, False] - ], - ids=[ - f"{x[0]}_{use_dtype}_{use_proxy}" - for x in ATTRIBUTES_SET - for use_dtype in [True, False] for use_proxy in [True, False] ], + ids=[f"{x[0]}_{use_proxy}" for x in ATTRIBUTES_SET for use_proxy in [True, False]], ) async def test_tango_signal_r( echo_device: str, @@ -449,13 +442,11 @@ async def test_tango_signal_r( py_type: Type[T], initial_value: T, put_value: T, - use_dtype: bool, use_proxy: bool, ): await prepare_device(echo_device, pv, initial_value) source = echo_device + "/" + pv proxy = await DeviceProxy(echo_device) if use_proxy else None - py_type = py_type if use_dtype else None timeout = 0.2 signal = tango_signal_r( @@ -473,7 +464,7 @@ async def test_tango_signal_r( # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( - "pv, tango_type, d_format, py_type, initial_value, put_value, use_dtype, use_proxy", + "pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy", [ ( pv, @@ -482,7 +473,6 @@ async def test_tango_signal_r( py_type, initial_value, put_value, - use_dtype, use_proxy, ) for ( @@ -493,15 +483,9 @@ async def test_tango_signal_r( initial_value, put_value, ) in ATTRIBUTES_SET - for use_dtype in [True, False] - for use_proxy in [True, False] - ], - ids=[ - f"{x[0]}_{use_dtype}_{use_proxy}" - for x in ATTRIBUTES_SET - for use_dtype in [True, False] for use_proxy in [True, False] ], + ids=[f"{x[0]}_{use_proxy}" for x in ATTRIBUTES_SET for use_proxy in [True, False]], ) async def test_tango_signal_w( echo_device: str, @@ -511,13 +495,11 @@ async def test_tango_signal_w( py_type: Type[T], initial_value: T, put_value: T, - use_dtype: bool, use_proxy: bool, ): await prepare_device(echo_device, pv, initial_value) source = echo_device + "/" + pv proxy = await DeviceProxy(echo_device) if use_proxy else None - py_type = py_type if use_dtype else None timeout = 0.2 signal = tango_signal_w( @@ -548,7 +530,7 @@ async def test_tango_signal_w( # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( - "pv, tango_type, d_format, py_type, initial_value, put_value, use_dtype, use_proxy", + "pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy", [ ( pv, @@ -557,7 +539,6 @@ async def test_tango_signal_w( py_type, initial_value, put_value, - use_dtype, use_proxy, ) for ( @@ -568,15 +549,9 @@ async def test_tango_signal_w( initial_value, put_value, ) in ATTRIBUTES_SET - for use_dtype in [True, False] - for use_proxy in [True, False] - ], - ids=[ - f"{x[0]}_{use_dtype}_{use_proxy}" - for x in ATTRIBUTES_SET - for use_dtype in [True, False] for use_proxy in [True, False] ], + ids=[f"{x[0]}_{use_proxy}" for x in ATTRIBUTES_SET for use_proxy in [True, False]], ) async def test_tango_signal_rw( echo_device: str, @@ -586,13 +561,11 @@ async def test_tango_signal_rw( py_type: Type[T], initial_value: T, put_value: T, - use_dtype: bool, use_proxy: bool, ): await prepare_device(echo_device, pv, initial_value) source = echo_device + "/" + pv proxy = await DeviceProxy(echo_device) if use_proxy else None - py_type = py_type if use_dtype else None timeout = 0.2 signal = tango_signal_rw( @@ -633,7 +606,7 @@ async def test_tango_signal_x(tango_test_device: str, use_proxy: bool): # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize( - "pv, tango_type, d_format, py_type, initial_value, put_value, use_dtype, use_proxy", + "pv, tango_type, d_format, py_type, initial_value, put_value, use_proxy", [ ( pv, @@ -642,7 +615,6 @@ async def test_tango_signal_x(tango_test_device: str, use_proxy: bool): py_type, initial_value, put_value, - use_dtype, use_proxy, ) for ( @@ -653,15 +625,9 @@ async def test_tango_signal_x(tango_test_device: str, use_proxy: bool): initial_value, put_value, ) in ATTRIBUTES_SET - for use_dtype in [True, False] - for use_proxy in [True, False] - ], - ids=[ - f"{x[0]}_{use_dtype}_{use_proxy}" - for x in ATTRIBUTES_SET - for use_dtype in [True, False] for use_proxy in [True, False] ], + ids=[f"{x[0]}_{use_proxy}" for x in ATTRIBUTES_SET for use_proxy in [True, False]], ) async def test_tango_signal_auto_attrs( echo_device: str, @@ -671,7 +637,6 @@ async def test_tango_signal_auto_attrs( py_type: Type[T], initial_value: T, put_value: T, - use_dtype: bool, use_proxy: bool, ): await prepare_device(echo_device, pv, initial_value) @@ -680,7 +645,7 @@ async def test_tango_signal_auto_attrs( timeout = 0.2 async def _test_signal(dtype, proxy): - signal = tango_signal_auto( + signal = await tango_signal_auto( datatype=dtype, trl=source, device_proxy=proxy, @@ -702,7 +667,7 @@ async def _test_signal(dtype, proxy): value = value.tolist() assert_close(value, put_value) - dtype = py_type if use_dtype else None + dtype = py_type await _test_signal(dtype, proxy) @@ -754,7 +719,7 @@ async def test_tango_signal_auto_cmds( timeout = 0.2 async def _test_signal(dtype, proxy): - signal = tango_signal_auto( + signal = await tango_signal_auto( datatype=dtype, trl=source, device_proxy=proxy, @@ -762,7 +727,7 @@ async def _test_signal(dtype, proxy): timeout=timeout, ) # Ophyd SignalX does not support types - assert type(signal) is SignalRW + assert type(signal) in [SignalR, SignalRW, SignalW] await signal.connect() assert signal reading = await signal.read() @@ -785,7 +750,7 @@ async def _test_signal(dtype, proxy): @pytest.mark.parametrize("use_proxy", [True, False]) async def test_tango_signal_auto_cmds_void(tango_test_device: str, use_proxy: bool): proxy = await DeviceProxy(tango_test_device) if use_proxy else None - signal = tango_signal_auto( + signal = await tango_signal_auto( datatype=None, trl=tango_test_device + "/" + "clear", device_proxy=proxy, @@ -799,9 +764,11 @@ async def test_tango_signal_auto_cmds_void(tango_test_device: str, use_proxy: bo # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_tango_signal_auto_badtrl(tango_test_device: str): + proxy = await DeviceProxy(tango_test_device) with pytest.raises(RuntimeError) as exc_info: - tango_signal_auto( + await tango_signal_auto( datatype=None, trl=tango_test_device + "/" + "badtrl", + device_proxy=proxy, ) assert f"Cannot find badtrl in {tango_test_device}" in str(exc_info.value) From e586a2418a452d8ee93706c821e1467b4165cfdb Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 12 Sep 2024 14:28:20 +0200 Subject: [PATCH 121/141] Improved test covereage of demo devices. Added composite device TangoDetector to demo which includes demonstrations of annotated subdevices. --- .../tango/base_devices/_base_device.py | 13 +++-- src/ophyd_async/tango/demo/__init__.py | 8 ++-- src/ophyd_async/tango/demo/_counter.py | 23 ++------- src/ophyd_async/tango/demo/_detector.py | 39 +++++++++++++++ src/ophyd_async/tango/demo/_mover.py | 19 -------- src/ophyd_async/tango/demo/_tango/_servers.py | 12 ----- tests/tango/test_base_device.py | 47 +++++-------------- 7 files changed, 66 insertions(+), 95 deletions(-) create mode 100644 src/ophyd_async/tango/demo/_detector.py diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index 3e03108597..473e63bb2f 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -55,9 +55,6 @@ def __init__( device_proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] = None, name: str = "", ) -> None: - if not trl and not device_proxy: - raise ValueError("Either 'trl' or 'device_proxy' must be provided.") - self.trl = trl if trl else "" self.proxy = device_proxy tango_create_children_from_annotations(self) @@ -77,12 +74,14 @@ async def closure(): raise RuntimeError("Could not connect to device proxy") from e return self - if self.trl in ["", None]: + if self.trl in ["", None] and self.proxy is not None: self.trl = self.proxy.name() - await closure() - self.register_signals() - await fill_proxy_entries(self) + if self.trl: + await closure() + if self.proxy is not None: + self.register_signals() + await fill_proxy_entries(self) # set_name should be called again to propagate the new signal names self.set_name(self.name) diff --git a/src/ophyd_async/tango/demo/__init__.py b/src/ophyd_async/tango/demo/__init__.py index 4334b401f6..78fffae2aa 100644 --- a/src/ophyd_async/tango/demo/__init__.py +++ b/src/ophyd_async/tango/demo/__init__.py @@ -1,12 +1,12 @@ -from ._counter import TangoCounter, TangoCounterConfig -from ._mover import TangoMover, TangoMoverConfig +from ._counter import TangoCounter +from ._detector import TangoDetector +from ._mover import TangoMover from ._tango import DemoCounter, DemoMover __all__ = [ "DemoCounter", "DemoMover", "TangoCounter", - "TangoCounterConfig", "TangoMover", - "TangoMoverConfig", + "TangoDetector", ] diff --git a/src/ophyd_async/tango/demo/_counter.py b/src/ophyd_async/tango/demo/_counter.py index 1a8ff7dc3d..4da1b2d17b 100644 --- a/src/ophyd_async/tango/demo/_counter.py +++ b/src/ophyd_async/tango/demo/_counter.py @@ -1,6 +1,3 @@ -from dataclasses import dataclass -from typing import Optional - from ophyd_async.core import ( DEFAULT_TIMEOUT, AsyncStatus, @@ -13,20 +10,16 @@ from ophyd_async.tango import TangoReadable, tango_polling -@dataclass -class TangoCounterConfig: - sample_time: Optional[float] = None - - # Enable device level polling, useful for servers that do not support events # Polling for individual signal can be enabled with a dict -@tango_polling((0.1, 0.1, 0.1), {"counts": (1.0, 0.1, 0.1)}) +@tango_polling({"counts": (1.0, 0.1, 0.1), "sample_time": (0.1, 0.1, 0.1)}) class TangoCounter(TangoReadable): # Enter the name and type of the signals you want to use # If type is None or Signal, the type will be inferred from the Tango device counts: SignalR[int] sample_time: SignalRW[float] start: SignalX + reset: SignalX def __init__(self, trl: str, name=""): super().__init__(trl, name=name) @@ -40,13 +33,5 @@ async def trigger(self) -> None: await self.start.trigger(wait=True, timeout=timeout) @AsyncStatus.wrap - async def prepare(self, value: TangoCounterConfig) -> None: - config = value.__dataclass_fields__ - for key in config: - v = getattr(value, key) - if v is not None: - if hasattr(self, key): - await getattr(self, key).set(v) - - def get_dataclass(self) -> TangoCounterConfig: - return TangoCounterConfig() + async def reset(self) -> None: + await self.reset.trigger(wait=True, timeout=DEFAULT_TIMEOUT) diff --git a/src/ophyd_async/tango/demo/_detector.py b/src/ophyd_async/tango/demo/_detector.py new file mode 100644 index 0000000000..089877ade3 --- /dev/null +++ b/src/ophyd_async/tango/demo/_detector.py @@ -0,0 +1,39 @@ +import asyncio + +from ophyd_async.core import ( + AsyncStatus, + DeviceVector, +) +from ophyd_async.tango import TangoReadable + +from . import TangoCounter, TangoMover + + +class TangoDetector(TangoReadable): + counters: DeviceVector[TangoCounter] + mover: TangoMover + + def __init__(self, *args, **kwargs): + if "counters_kwargs" in kwargs: + self._counters_kwargs = kwargs.pop("counters_kwargs") + if "mover_kwargs" in kwargs: + self._mover_kwargs = kwargs.pop("mover_kwargs") + super().__init__(*args, **kwargs) + self.add_readables([self.counters, self.mover]) + + def set(self, value): + return self.mover.set(value) + + def stop(self): + return self.mover.stop() + + @AsyncStatus.wrap + async def trigger(self): + statuses = [] + for counter in self.counters.values(): + statuses.append(counter.reset()) + await asyncio.gather(*statuses) + statuses.clear() + for counter in self.counters.values(): + statuses.append(counter.trigger()) + await asyncio.gather(*statuses) diff --git a/src/ophyd_async/tango/demo/_mover.py b/src/ophyd_async/tango/demo/_mover.py index bf54f6b014..8168093b2b 100644 --- a/src/ophyd_async/tango/demo/_mover.py +++ b/src/ophyd_async/tango/demo/_mover.py @@ -1,6 +1,4 @@ import asyncio -from dataclasses import dataclass -from typing import Optional from bluesky.protocols import Movable, Reading, Stoppable @@ -21,11 +19,6 @@ from tango import DevState -@dataclass -class TangoMoverConfig: - velocity: Optional[float] = None - - # Enable device level polling, useful for servers that do not support events @tango_polling((0.1, 0.1, 0.1)) class TangoMover(TangoReadable, Movable, Stoppable): @@ -88,15 +81,3 @@ def _wait(value: dict[str, Reading]): def stop(self, success: bool = True) -> AsyncStatus: self._set_success = success return self._stop.trigger() - - @AsyncStatus.wrap - async def prepare(self, value: TangoMoverConfig) -> None: - config = value.__dataclass_fields__ - for key in config: - v = getattr(value, key) - if v is not None: - if hasattr(self, key): - await getattr(self, key).set(v) - - def get_dataclass(self) -> TangoMoverConfig: - return TangoMoverConfig() diff --git a/src/ophyd_async/tango/demo/_tango/_servers.py b/src/ophyd_async/tango/demo/_tango/_servers.py index f253228908..d3332f881a 100644 --- a/src/ophyd_async/tango/demo/_tango/_servers.py +++ b/src/ophyd_async/tango/demo/_tango/_servers.py @@ -25,13 +25,6 @@ async def write_position(self, new_position): self._setpoint = new_position await self.move() - @attribute(dtype=float, access=AttrWriteType.READ_WRITE) - async def setpoint(self): - return self._setpoint - - async def write_setpoint(self, new_position): - self._setpoint = new_position - @attribute(dtype=float, access=AttrWriteType.READ_WRITE) async def velocity(self): return self._velocity @@ -39,9 +32,6 @@ async def velocity(self): async def write_velocity(self, value: float): self._velocity = value - async def write_precision(self, value: float): - self._precision = value - @attribute(dtype=DevState, access=AttrWriteType.READ) async def state(self): return self.get_state() @@ -88,8 +78,6 @@ async def sample_time(self): return self._sample_time async def write_sample_time(self, value: float): - if value < 0.0: - raise ValueError("Sample time must be a positive number") self._sample_time = value @attribute(dtype=DevState, access=AttrWriteType.READ) diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index d47d62e5e7..3071d60282 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -16,8 +16,7 @@ from ophyd_async.tango.demo import ( DemoCounter, DemoMover, - TangoCounter, - TangoMover, + TangoDetector, ) from tango import ( AttrDataFormat, @@ -363,43 +362,23 @@ async def test_with_bluesky(tango_test_device): @pytest.mark.asyncio async def test_tango_demo(demo_test_context): with demo_test_context: - motor1 = TangoMover( - trl=demo_test_context.get_device_access("demo/motor/1"), name="motor1" + detector = TangoDetector( + trl="", + name="detector", + counters_kwargs={"prefix": "demo/counter/", "count": 2}, + mover_kwargs={"trl": "demo/motor/1"}, ) - counter1 = TangoCounter( - trl=demo_test_context.get_device_access("demo/counter/1"), name="counter1" - ) - counter2 = TangoCounter( - trl=demo_test_context.get_device_access("demo/counter/2"), name="counter2" - ) - await motor1.connect() - await counter1.connect() - await counter2.connect() + await detector.connect() RE = RunEngine() - dc = motor1.get_dataclass() - dc.velocity = 1.0 - prepare_status = motor1.prepare(dc) - await prepare_status - assert all([prepare_status.done, prepare_status.success]) - - cc = counter1.get_dataclass() - cc.sample_time = 0.1 - prepare_status1 = counter1.prepare(cc) - prepare_status2 = counter2.prepare(cc) - await prepare_status1 - await prepare_status2 - assert all([prepare_status1.done, prepare_status1.success]) - assert all([prepare_status2.done, prepare_status2.success]) - - RE(bps.read(motor1.position)) - RE(bps.mv(motor1, 0)) - RE(bp.count([counter1, counter2])) - - set_status = motor1.set(1.0) + RE(bps.read(detector)) + RE(bps.mv(detector, 0)) + RE(bp.count(list(detector.counters.values()))) + + set_status = detector.set(1.0) await asyncio.sleep(0.1) - stop_status = motor1.stop() + stop_status = detector.stop() await set_status await stop_status assert all([set_status.done, stop_status.done]) From 73f8e5d2282f808b1331b07b826267eff012e356 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 12 Sep 2024 14:34:45 +0200 Subject: [PATCH 122/141] Updated tango demo in docs --- docs/examples/tango_demo.py | 41 +++++++++++++------------ src/ophyd_async/tango/demo/_detector.py | 7 +++-- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/docs/examples/tango_demo.py b/docs/examples/tango_demo.py index 3a978fde04..503dccd1ec 100644 --- a/docs/examples/tango_demo.py +++ b/docs/examples/tango_demo.py @@ -1,15 +1,13 @@ import asyncio +import bluesky.plan_stubs as bps import bluesky.plans as bp from bluesky import RunEngine -from bluesky.callbacks.best_effort import BestEffortCallback -from bluesky.utils import ProgressBarManager from ophyd_async.tango.demo import ( DemoCounter, DemoMover, - TangoCounter, - TangoMover, + TangoDetector, ) from tango.test_context import MultiDeviceTestContext @@ -28,25 +26,28 @@ async def main(): - with tango_context as context: - motor1 = TangoMover( - trl=context.get_device_access("demo/motor/1"), name="motor1" + with tango_context: + detector = TangoDetector( + trl="", + name="detector", + counters_kwargs={"prefix": "demo/counter/", "count": 2}, + mover_kwargs={"trl": "demo/motor/1"}, ) - counter1 = TangoCounter( - trl=context.get_device_access("demo/counter/1"), name="counter1" - ) - counter2 = TangoCounter( - trl=context.get_device_access("demo/counter/2"), name="counter2" - ) - await motor1.connect() - await counter1.connect() - await counter2.connect() + await detector.connect() RE = RunEngine() - RE.subscribe(BestEffortCallback()) - RE.waiting_hook = ProgressBarManager() - # RE(bps.mv(motor1, 1)) - RE(bp.scan([counter1, counter2], motor1, -1, 1, 10)) + + RE(bps.read(detector)) + RE(bps.mv(detector, 0)) + RE(bp.count(list(detector.counters.values()))) + + set_status = detector.set(1.0) + await asyncio.sleep(0.1) + stop_status = detector.stop() + await set_status + await stop_status + assert all([set_status.done, stop_status.done]) + assert all([set_status.success, stop_status.success]) if __name__ == "__main__": diff --git a/src/ophyd_async/tango/demo/_detector.py b/src/ophyd_async/tango/demo/_detector.py index 089877ade3..85fd31bdef 100644 --- a/src/ophyd_async/tango/demo/_detector.py +++ b/src/ophyd_async/tango/demo/_detector.py @@ -6,7 +6,8 @@ ) from ophyd_async.tango import TangoReadable -from . import TangoCounter, TangoMover +from ._counter import TangoCounter +from ._mover import TangoMover class TangoDetector(TangoReadable): @@ -24,8 +25,8 @@ def __init__(self, *args, **kwargs): def set(self, value): return self.mover.set(value) - def stop(self): - return self.mover.stop() + def stop(self, success: bool = True) -> AsyncStatus: + return self.mover.stop(success) @AsyncStatus.wrap async def trigger(self): From b5f97871cb1d312b4491977ee78177525231f396 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 12 Sep 2024 14:39:16 +0200 Subject: [PATCH 123/141] Fix tests to allow for TangoReadables without a trl/proxy. --- tests/tango/test_base_device.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 3071d60282..642e5e8248 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -314,11 +314,6 @@ def compare_values(expected, received): async def test_connect(tango_test_device): values, description = await describe_class(tango_test_device) - with pytest.raises(ValueError) as excinfo: - async with DeviceCollector(): - TestTangoReadable() - assert "Either 'trl' or 'device_proxy' must be provided." in str(excinfo.value) - async with DeviceCollector(): test_device = TestTangoReadable(tango_test_device) @@ -343,9 +338,8 @@ async def test_connect_proxy(tango_test_device, proxy: Optional[bool]): assert isinstance(test_device.proxy, tango._tango.DeviceProxy) else: proxy = None - with pytest.raises(ValueError) as excinfo: - test_device = TestTangoReadable(device_proxy=proxy) - assert "Either 'trl' or 'device_proxy' must be provided." in str(excinfo.value) + test_device = TestTangoReadable(device_proxy=proxy) + assert test_device # -------------------------------------------------------------------- From 6d4284aa2118b795da79311e1840395f9ed0c8c8 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 18 Sep 2024 10:47:47 +0200 Subject: [PATCH 124/141] Refactored to allow for creation of tango devices without a trl such as when tango devices are initialized from annotations. If this is done, the trl of the device must be set before connect is called. This should be done in the top level device init as shown in the demo. Signals which are initialized from annotations should have the same name as their tango attribute/command but may be prepended by an underscore. This underscore is stripped when setting the backend trl. This is so that a signal called _stop which corresponds to the tango command stop can exist in a Moveable without overwriting its stop method. Signal trls will automatically be set during commit by using its device trl. --- .../tango/base_devices/_base_device.py | 56 +++++++------------ src/ophyd_async/tango/demo/_counter.py | 4 +- src/ophyd_async/tango/demo/_detector.py | 19 ++++--- src/ophyd_async/tango/demo/_mover.py | 3 +- src/ophyd_async/tango/signal/_signal.py | 4 +- .../tango/signal/_tango_transport.py | 17 ++++-- tests/tango/test_base_device.py | 4 +- 7 files changed, 55 insertions(+), 52 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index 473e63bb2f..5bc7ea4bc0 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -60,25 +60,30 @@ def __init__( tango_create_children_from_annotations(self) super().__init__(name=name) + def set_trl(self, trl: str): + """Set the Tango resource locator.""" + if not isinstance(trl, str): + raise ValueError("TRL must be a string.") + self.trl = trl + async def connect( self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT, force_reconnect: bool = False, ): - async def closure(): - try: - if self.proxy is None: - self.proxy = await AsyncDeviceProxy(self.trl) - except Exception as e: - raise RuntimeError("Could not connect to device proxy") from e - return self - - if self.trl in ["", None] and self.proxy is not None: + if self.trl and self.proxy is None: + self.proxy = await AsyncDeviceProxy(self.trl) + elif self.proxy and not self.trl: self.trl = self.proxy.name() - if self.trl: - await closure() + # Set the trl of the signal backends + for child in self.children(): + if isinstance(child[1], Signal): + resource_name = child[0].lstrip("_") + read_trl = f"{self.trl}/{resource_name}" + child[1]._backend.set_trl(read_trl, read_trl) # noqa: SLF001 + if self.proxy is not None: self.register_signals() await fill_proxy_entries(self) @@ -167,9 +172,7 @@ def decorator(cls): def tango_create_children_from_annotations( - device: TangoDevice, - included_optional_fields: Tuple[str, ...] = (), - device_vectors: Optional[Dict[str, int]] = None, + device: TangoDevice, included_optional_fields: Tuple[str, ...] = () ): """Initialize blocks at __init__ of `device`.""" for name, device_type in get_type_hints(type(device)).items(): @@ -182,13 +185,7 @@ def tango_create_children_from_annotations( is_device_vector, device_type = _strip_device_vector(device_type) if is_device_vector: - kwargs = "_" + name + "_kwargs" - kwargs = getattr(device, kwargs, {}) - prefix = kwargs["prefix"] - count = kwargs["count"] - n_device_vector = DeviceVector( - {i: device_type(f"{prefix}{i}") for i in range(1, count + 1)} - ) + n_device_vector = DeviceVector() setattr(device, name, n_device_vector) else: @@ -196,24 +193,13 @@ def tango_create_children_from_annotations( origin = origin if origin else device_type if issubclass(origin, Signal): - datatype = None - tango_name = name.lstrip("_") - read_trl = f"{device.trl}/{tango_name}" type_args = get_args(device_type) - if type_args: - datatype = type_args[0] - backend = make_backend( - datatype=datatype, - read_trl=read_trl, - write_trl=read_trl, - device_proxy=device.proxy, - ) + datatype = type_args[0] if type_args else None + backend = make_backend(datatype=datatype, device_proxy=device.proxy) setattr(device, name, origin(name=name, backend=backend)) elif issubclass(origin, Device) or isinstance(origin, Device): - kwargs = "_" + name + "_kwargs" - kwargs = getattr(device, kwargs, "") - setattr(device, name, origin(**kwargs)) + setattr(device, name, origin()) async def fill_proxy_entries(device: TangoDevice): diff --git a/src/ophyd_async/tango/demo/_counter.py b/src/ophyd_async/tango/demo/_counter.py index 4da1b2d17b..3a6892b21f 100644 --- a/src/ophyd_async/tango/demo/_counter.py +++ b/src/ophyd_async/tango/demo/_counter.py @@ -1,3 +1,5 @@ +from typing import Optional + from ophyd_async.core import ( DEFAULT_TIMEOUT, AsyncStatus, @@ -21,7 +23,7 @@ class TangoCounter(TangoReadable): start: SignalX reset: SignalX - def __init__(self, trl: str, name=""): + def __init__(self, trl: Optional[str] = "", name=""): super().__init__(trl, name=name) self.add_readables([self.counts], HintedSignal) self.add_readables([self.sample_time], ConfigSignal) diff --git a/src/ophyd_async/tango/demo/_detector.py b/src/ophyd_async/tango/demo/_detector.py index 85fd31bdef..dc88f0c11a 100644 --- a/src/ophyd_async/tango/demo/_detector.py +++ b/src/ophyd_async/tango/demo/_detector.py @@ -11,15 +11,20 @@ class TangoDetector(TangoReadable): - counters: DeviceVector[TangoCounter] + counters: DeviceVector mover: TangoMover - def __init__(self, *args, **kwargs): - if "counters_kwargs" in kwargs: - self._counters_kwargs = kwargs.pop("counters_kwargs") - if "mover_kwargs" in kwargs: - self._mover_kwargs = kwargs.pop("mover_kwargs") - super().__init__(*args, **kwargs) + def __init__(self, trl: str, mover_trl: str, counter_trls: list[str], name=""): + super().__init__(trl, name=name) + + # If devices are inferred from type hints, they will be created automatically + # during init. If they are created automatically, their trl must be set before + # they are connected. + self.mover.set_trl(mover_trl) + for i, c_trl in enumerate(counter_trls): + self.counters[i + 1] = TangoCounter(c_trl) + + # Define the readables for TangoDetector self.add_readables([self.counters, self.mover]) def set(self, value): diff --git a/src/ophyd_async/tango/demo/_mover.py b/src/ophyd_async/tango/demo/_mover.py index 8168093b2b..ec1717020a 100644 --- a/src/ophyd_async/tango/demo/_mover.py +++ b/src/ophyd_async/tango/demo/_mover.py @@ -1,4 +1,5 @@ import asyncio +from typing import Optional from bluesky.protocols import Movable, Reading, Stoppable @@ -28,7 +29,7 @@ class TangoMover(TangoReadable, Movable, Stoppable): velocity: SignalRW[float] _stop: SignalX - def __init__(self, trl: str, name=""): + def __init__(self, trl: Optional[str] = "", name=""): super().__init__(trl, name=name) self.add_readables([self.position], HintedSignal) self.add_readables([self.velocity], ConfigSignal) diff --git a/src/ophyd_async/tango/signal/_signal.py b/src/ophyd_async/tango/signal/_signal.py index 2395e96eaf..0875b968ff 100644 --- a/src/ophyd_async/tango/signal/_signal.py +++ b/src/ophyd_async/tango/signal/_signal.py @@ -18,8 +18,8 @@ def make_backend( datatype: Optional[Type[T]], - read_trl: str, - write_trl: str, + read_trl: Optional[str] = "", + write_trl: Optional[str] = "", device_proxy: Optional[DeviceProxy] = None, ) -> TangoSignalBackend: return TangoSignalBackend(datatype, read_trl, write_trl, device_proxy) diff --git a/src/ophyd_async/tango/signal/_tango_transport.py b/src/ophyd_async/tango/signal/_tango_transport.py index 8d7742b5c8..89a0f0007a 100644 --- a/src/ophyd_async/tango/signal/_tango_transport.py +++ b/src/ophyd_async/tango/signal/_tango_transport.py @@ -580,16 +580,17 @@ class TangoSignalBackend(SignalBackend[T]): def __init__( self, datatype: Optional[Type[T]], - read_trl: str, - write_trl: str, + read_trl: Optional[str] = "", + write_trl: Optional[str] = "", device_proxy: Optional[DeviceProxy] = None, ): + self.device_proxy = device_proxy self.datatype = datatype self.read_trl = read_trl self.write_trl = write_trl self.proxies: Dict[str, TangoProxy] = { - read_trl: device_proxy, - write_trl: device_proxy, + read_trl: self.device_proxy, + write_trl: self.device_proxy, } self.trl_configs: Dict[str, AttributeInfoEx] = {} self.descriptor: Descriptor = {} # type: ignore @@ -597,6 +598,14 @@ def __init__( self.support_events = True self.status = None + def set_trl(self, read_trl: str, write_trl: Optional[str] = ""): + self.read_trl = read_trl + self.write_trl = write_trl if write_trl else read_trl + self.proxies: Dict[str, TangoProxy] = { + read_trl: self.device_proxy, + write_trl: self.device_proxy, + } + def source(self, name: str) -> str: return self.read_trl diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 642e5e8248..f2cc2cc168 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -359,8 +359,8 @@ async def test_tango_demo(demo_test_context): detector = TangoDetector( trl="", name="detector", - counters_kwargs={"prefix": "demo/counter/", "count": 2}, - mover_kwargs={"trl": "demo/motor/1"}, + mover_trl="demo/motor/1", + counter_trls=["demo/counter/1", "demo/counter/2"], ) await detector.connect() From 6cdaca8dccc669e79e611b696c079f25a8194fa4 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 18 Sep 2024 10:58:23 +0200 Subject: [PATCH 125/141] Replaced wait_for_idle in the demo motor with an AsyncStatus wrapped call to wait_for_value --- src/ophyd_async/tango/demo/_mover.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/ophyd_async/tango/demo/_mover.py b/src/ophyd_async/tango/demo/_mover.py index ec1717020a..582aa5e344 100644 --- a/src/ophyd_async/tango/demo/_mover.py +++ b/src/ophyd_async/tango/demo/_mover.py @@ -1,7 +1,7 @@ import asyncio from typing import Optional -from bluesky.protocols import Movable, Reading, Stoppable +from bluesky.protocols import Movable, Stoppable from ophyd_async.core import ( DEFAULT_TIMEOUT, @@ -15,6 +15,7 @@ WatchableAsyncStatus, WatcherUpdate, observe_value, + wait_for_value, ) from ophyd_async.tango import TangoReadable, tango_polling from tango import DevState @@ -48,8 +49,9 @@ async def set(self, value: float, timeout: CalculatableTimeout = CalculateTimeou # For this server, set returns immediately so this status should not be awaited await self.position.set(value, wait=False, timeout=timeout) - # Wait for the motor to stop - move_status = self.wait_for_idle() + move_status = AsyncStatus( + wait_for_value(self.state, DevState.ON, timeout=timeout) + ) try: async for current_position in observe_value( @@ -67,18 +69,6 @@ async def set(self, value: float, timeout: CalculatableTimeout = CalculateTimeou if not self._set_success: raise RuntimeError("Motor was stopped") - @AsyncStatus.wrap - async def wait_for_idle(self): - event = asyncio.Event() - - def _wait(value: dict[str, Reading]): - if value[self.state.name]["value"] == DevState.ON: - event.set() - - self.state.subscribe(_wait) - await event.wait() - self.state.clear_sub(_wait) - def stop(self, success: bool = True) -> AsyncStatus: self._set_success = success return self._stop.trigger() From 1e8a00a0ce062fb94c49df7e4d34f0fd2f802927 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 19 Sep 2024 11:14:37 +0200 Subject: [PATCH 126/141] New formatting to comply with ruff --- .../tango/base_devices/_base_device.py | 30 +++-- .../tango/base_devices/_tango_readable.py | 9 +- src/ophyd_async/tango/demo/_counter.py | 4 +- src/ophyd_async/tango/demo/_mover.py | 9 +- src/ophyd_async/tango/signal/_signal.py | 41 ++++--- .../tango/signal/_tango_transport.py | 104 +++++++++--------- tests/tango/test_base_device.py | 9 +- tests/tango/test_tango_signals.py | 32 +++--- 8 files changed, 117 insertions(+), 121 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index 5bc7ea4bc0..db009a8fb9 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -1,10 +1,6 @@ from __future__ import annotations from typing import ( - Dict, - Optional, - Tuple, - Type, TypeVar, Union, get_args, @@ -22,7 +18,7 @@ make_backend, tango_signal_auto, ) -from tango import DeviceProxy as SyncDeviceProxy +from tango import DeviceProxy as DeviceProxy from tango.asyncio import DeviceProxy as AsyncDeviceProxy T = TypeVar("T") @@ -44,15 +40,15 @@ class TangoDevice(Device): """ trl: str = "" - proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] = None - _polling: Tuple[bool, float, float, float] = (False, 0.1, None, 0.1) - _signal_polling: Dict[str, Tuple[bool, float, float, float]] = {} + proxy: DeviceProxy | None = None + _polling: tuple[bool, float, float, float] = (False, 0.1, None, 0.1) + _signal_polling: dict[str, tuple[bool, float, float, float]] = {} _poll_only_annotated_signals: bool = True def __init__( self, - trl: Optional[str] = None, - device_proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] = None, + trl: str | None = None, + device_proxy: DeviceProxy | None = None, name: str = "", ) -> None: self.trl = trl if trl else "" @@ -112,10 +108,10 @@ def register_signals(self): def tango_polling( - polling: Optional[ - Union[Tuple[float, float, float], Dict[str, Tuple[float, float, float]]] - ] = None, - signal_polling: Optional[Dict[str, Tuple[float, float, float]]] = None, + polling: tuple[float, float, float] + | dict[str, tuple[float, float, float]] + | None = None, + signal_polling: dict[str, tuple[float, float, float]] | None = None, ): """ Class decorator to configure polling for Tango devices. @@ -172,7 +168,7 @@ def decorator(cls): def tango_create_children_from_annotations( - device: TangoDevice, included_optional_fields: Tuple[str, ...] = () + device: TangoDevice, included_optional_fields: tuple[str, ...] = () ): """Initialize blocks at __init__ of `device`.""" for name, device_type in get_type_hints(type(device)).items(): @@ -227,7 +223,7 @@ async def fill_proxy_entries(device: TangoDevice): raise e -def _strip_union(field: Union[Union[T], T]) -> Tuple[T, bool]: +def _strip_union(field: T | T) -> tuple[T, bool]: if get_origin(field) is Union: args = get_args(field) is_optional = type(None) in args @@ -237,7 +233,7 @@ def _strip_union(field: Union[Union[T], T]) -> Tuple[T, bool]: return field, False -def _strip_device_vector(field: Union[Type[Device]]) -> Tuple[bool, Type[Device]]: +def _strip_device_vector(field: type[Device]) -> tuple[bool, type[Device]]: if get_origin(field) is DeviceVector: return True, get_args(field)[0] return False, field diff --git a/src/ophyd_async/tango/base_devices/_tango_readable.py b/src/ophyd_async/tango/base_devices/_tango_readable.py index 94e85d96a3..44bfc93888 100644 --- a/src/ophyd_async/tango/base_devices/_tango_readable.py +++ b/src/ophyd_async/tango/base_devices/_tango_readable.py @@ -1,13 +1,10 @@ from __future__ import annotations -from typing import Optional, Union - from ophyd_async.core import ( StandardReadable, ) from ophyd_async.tango.base_devices._base_device import TangoDevice -from tango import DeviceProxy as SyncDeviceProxy -from tango.asyncio import DeviceProxy as AsyncDeviceProxy +from tango import DeviceProxy class TangoReadable(TangoDevice, StandardReadable): @@ -26,8 +23,8 @@ class TangoReadable(TangoDevice, StandardReadable): def __init__( self, - trl: Optional[str] = None, - device_proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] = None, + trl: str | None = None, + device_proxy: DeviceProxy | None = None, name: str = "", ) -> None: TangoDevice.__init__(self, trl, device_proxy=device_proxy, name=name) diff --git a/src/ophyd_async/tango/demo/_counter.py b/src/ophyd_async/tango/demo/_counter.py index 3a6892b21f..20d6bdcb74 100644 --- a/src/ophyd_async/tango/demo/_counter.py +++ b/src/ophyd_async/tango/demo/_counter.py @@ -1,5 +1,3 @@ -from typing import Optional - from ophyd_async.core import ( DEFAULT_TIMEOUT, AsyncStatus, @@ -23,7 +21,7 @@ class TangoCounter(TangoReadable): start: SignalX reset: SignalX - def __init__(self, trl: Optional[str] = "", name=""): + def __init__(self, trl: str | None = "", name=""): super().__init__(trl, name=name) self.add_readables([self.counts], HintedSignal) self.add_readables([self.sample_time], ConfigSignal) diff --git a/src/ophyd_async/tango/demo/_mover.py b/src/ophyd_async/tango/demo/_mover.py index 582aa5e344..8705e0b57f 100644 --- a/src/ophyd_async/tango/demo/_mover.py +++ b/src/ophyd_async/tango/demo/_mover.py @@ -1,13 +1,12 @@ import asyncio -from typing import Optional from bluesky.protocols import Movable, Stoppable from ophyd_async.core import ( + CALCULATE_TIMEOUT, DEFAULT_TIMEOUT, AsyncStatus, CalculatableTimeout, - CalculateTimeout, ConfigSignal, HintedSignal, SignalRW, @@ -30,19 +29,19 @@ class TangoMover(TangoReadable, Movable, Stoppable): velocity: SignalRW[float] _stop: SignalX - def __init__(self, trl: Optional[str] = "", name=""): + def __init__(self, trl: str | None = "", name=""): super().__init__(trl, name=name) self.add_readables([self.position], HintedSignal) self.add_readables([self.velocity], ConfigSignal) self._set_success = True @WatchableAsyncStatus.wrap - async def set(self, value: float, timeout: CalculatableTimeout = CalculateTimeout): + async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT): self._set_success = True (old_position, velocity) = await asyncio.gather( self.position.get_value(), self.velocity.get_value() ) - if timeout is CalculateTimeout: + if timeout is CALCULATE_TIMEOUT: assert velocity > 0, "Motor has zero velocity" timeout = abs(value - old_position) / velocity + DEFAULT_TIMEOUT diff --git a/src/ophyd_async/tango/signal/_signal.py b/src/ophyd_async/tango/signal/_signal.py index 0875b968ff..7698290efa 100644 --- a/src/ophyd_async/tango/signal/_signal.py +++ b/src/ophyd_async/tango/signal/_signal.py @@ -3,7 +3,6 @@ from __future__ import annotations from enum import Enum, IntEnum -from typing import Optional, Type, Union import numpy.typing as npt @@ -12,24 +11,24 @@ TangoSignalBackend, get_python_type, ) -from tango import AttrDataFormat, AttrWriteType, CmdArgType, DevState -from tango.asyncio import DeviceProxy +from tango import AttrDataFormat, AttrWriteType, CmdArgType, DeviceProxy, DevState +from tango.asyncio import DeviceProxy as AsyncDeviceProxy def make_backend( - datatype: Optional[Type[T]], - read_trl: Optional[str] = "", - write_trl: Optional[str] = "", - device_proxy: Optional[DeviceProxy] = None, + datatype: type[T] | None, + read_trl: str | None = "", + write_trl: str | None = "", + device_proxy: DeviceProxy | None = None, ) -> TangoSignalBackend: return TangoSignalBackend(datatype, read_trl, write_trl, device_proxy) def tango_signal_rw( - datatype: Type[T], + datatype: type[T], read_trl: str, - write_trl: Optional[str] = None, - device_proxy: Optional[DeviceProxy] = None, + write_trl: str | None = None, + device_proxy: DeviceProxy | None = None, timeout: float = DEFAULT_TIMEOUT, name: str = "", ) -> SignalRW[T]: @@ -55,9 +54,9 @@ def tango_signal_rw( def tango_signal_r( - datatype: Type[T], + datatype: type[T], read_trl: str, - device_proxy: Optional[DeviceProxy] = None, + device_proxy: DeviceProxy | None = None, timeout: float = DEFAULT_TIMEOUT, name: str = "", ) -> SignalR[T]: @@ -81,9 +80,9 @@ def tango_signal_r( def tango_signal_w( - datatype: Type[T], + datatype: type[T], write_trl: str, - device_proxy: Optional[DeviceProxy] = None, + device_proxy: DeviceProxy | None = None, timeout: float = DEFAULT_TIMEOUT, name: str = "", ) -> SignalW[T]: @@ -108,7 +107,7 @@ def tango_signal_w( def tango_signal_x( write_trl: str, - device_proxy: Optional[DeviceProxy] = None, + device_proxy: DeviceProxy | None = None, timeout: float = DEFAULT_TIMEOUT, name: str = "", ) -> SignalX: @@ -130,13 +129,13 @@ def tango_signal_x( async def tango_signal_auto( - datatype: Optional[Type[T]] = None, + datatype: type[T] | None = None, *, trl: str, - device_proxy: Optional[DeviceProxy], + device_proxy: DeviceProxy | None, timeout: float = DEFAULT_TIMEOUT, name: str = "", -) -> Union[SignalW, SignalX, SignalR, SignalRW, None]: +) -> SignalW | SignalX | SignalR | SignalRW | None: try: signal_character = await infer_signal_character(trl, device_proxy) except RuntimeError as e: @@ -159,10 +158,10 @@ async def tango_signal_auto( return SignalX(backend=backend, timeout=timeout, name=name) -async def infer_python_type(trl: str = "", proxy: DeviceProxy = None) -> Type[T]: +async def infer_python_type(trl: str = "", proxy: DeviceProxy = None) -> type[T]: device_trl, tr_name = trl.rsplit("/", 1) if proxy is None: - dev_proxy = await DeviceProxy(device_trl) + dev_proxy = await AsyncDeviceProxy(device_trl) else: dev_proxy = proxy @@ -189,7 +188,7 @@ async def infer_python_type(trl: str = "", proxy: DeviceProxy = None) -> Type[T] async def infer_signal_character(trl, proxy: DeviceProxy = None) -> str: device_trl, tr_name = trl.rsplit("/", 1) if proxy is None: - dev_proxy = await DeviceProxy(device_trl) + dev_proxy = await AsyncDeviceProxy(device_trl) else: dev_proxy = proxy diff --git a/src/ophyd_async/tango/signal/_tango_transport.py b/src/ophyd_async/tango/signal/_tango_transport.py index 89a0f0007a..e9ae18dc77 100644 --- a/src/ophyd_async/tango/signal/_tango_transport.py +++ b/src/ophyd_async/tango/signal/_tango_transport.py @@ -4,7 +4,6 @@ from abc import abstractmethod from asyncio import CancelledError from enum import Enum -from typing import Dict, Optional, Type, Union import numpy as np from bluesky.protocols import DataKey, Descriptor, Reading @@ -26,10 +25,11 @@ CmdArgType, CommandInfo, DevFailed, + DeviceProxy, DevState, EventType, ) -from tango.asyncio import DeviceProxy +from tango.asyncio import DeviceProxy as AsyncDeviceProxy from tango.asyncio_executor import ( AsyncioExecutor, get_global_executor, @@ -96,12 +96,12 @@ async def get_w_value(self) -> T: @abstractmethod async def put( - self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None + self, value: T | None, wait: bool = True, timeout: float | None = None ) -> None: """Put value to TRL""" @abstractmethod - async def get_config(self) -> Union[AttributeInfoEx, CommandInfo]: + async def get_config(self) -> AttributeInfoEx | CommandInfo: """Get TRL config async""" @abstractmethod @@ -112,7 +112,7 @@ def has_subscription(self) -> bool: """indicates, that this trl already subscribed""" @abstractmethod - def subscribe_callback(self, callback: Optional[ReadingValueCallback]): + def subscribe_callback(self, callback: ReadingValueCallback | None): """subscribe tango CHANGE event to callback""" @abstractmethod @@ -164,7 +164,7 @@ async def get_w_value(self) -> T: @ensure_proper_executor async def put( - self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None + self, value: T | None, wait: bool = True, timeout: float | None = None ) -> None or AsyncStatus: if wait: try: @@ -174,10 +174,12 @@ async def _write(): task = asyncio.create_task(_write()) await asyncio.wait_for(task, timeout) - except asyncio.TimeoutError: - raise TimeoutError(f"{self._name} attr put failed: Timeout") - except DevFailed as e: - raise RuntimeError(f"{self._name} device failure: {e.args[0].desc}") + except asyncio.TimeoutError as te: + raise TimeoutError(f"{self._name} attr put failed: Timeout") from te + except DevFailed as de: + raise RuntimeError( + f"{self._name} device" f" failure: {de.args[0].desc}" + ) from de else: rid = await self._proxy.write_attribute_asynch(self._name, value) @@ -194,11 +196,11 @@ async def wait_for_reply(rd, to): if to and time.time() - start_time > to: raise TimeoutError( f"{self._name} attr put failed:" f" Timeout" - ) + ) from exc else: raise RuntimeError( f"{self._name} device failure:" f" {exc.args[0].desc}" - ) + ) from exc return AsyncStatus(wait_for_reply(rid, timeout)) @@ -218,7 +220,7 @@ async def get_reading(self) -> Reading: def has_subscription(self) -> bool: return bool(self._callback) - def subscribe_callback(self, callback: Optional[ReadingValueCallback]): + def subscribe_callback(self, callback: ReadingValueCallback | None): # If the attribute supports events, then we can subscribe to them # If the callback is not a callable, then we raise an error if callback is not None and not callable(callback): @@ -293,11 +295,11 @@ async def poll(self): if self._callback is not None: self._callback(last_reading, last_reading["value"]) except Exception as e: - raise RuntimeError(f"Could not poll the attribute: {e}") + raise RuntimeError(f"Could not poll the attribute: {e}") from e try: # If the value is a number, we can check for changes - if isinstance(last_reading["value"], (int, float)): + if isinstance(last_reading["value"], int | float): while True: await asyncio.sleep(self._polling_period) reading = await self.get_reading() @@ -350,7 +352,7 @@ async def poll(self): break last_reading = reading.copy() except Exception as e: - raise RuntimeError(f"Could not poll the attribute: {e}") + raise RuntimeError(f"Could not poll the attribute: {e}") from e def set_polling( self, @@ -380,7 +382,7 @@ async def get_w_value(self) -> T: @ensure_proper_executor async def put( - self, value: Optional[T], wait: bool = True, timeout: Optional[float] = None + self, value: T | None, wait: bool = True, timeout: float | None = None ) -> None or AsyncStatus: if wait: try: @@ -395,10 +397,12 @@ async def _put(): "timestamp": time.time(), "alarm_severity": 0, } - except asyncio.TimeoutError: - raise TimeoutError(f"{self._name} command failed: Timeout") - except DevFailed as e: - raise RuntimeError(f"{self._name} device failure: {e.args[0].desc}") + except asyncio.TimeoutError as te: + raise TimeoutError(f"{self._name} command failed: Timeout") from te + except DevFailed as de: + raise RuntimeError( + f"{self._name} device" f" failure: {de.args[0].desc}" + ) from de else: rid = self._proxy.command_inout_asynch(self._name, value) @@ -415,17 +419,17 @@ async def wait_for_reply(rd, to): "alarm_severity": 0, } break - except DevFailed as e: - if e.args[0].reason == "API_AsynReplyNotArrived": + except DevFailed as de: + if de.args[0].reason == "API_AsynReplyNotArrived": await asyncio.sleep(A_BIT) if to and time.time() - start_time > to: raise TimeoutError( "Timeout while waiting for command reply" - ) + ) from de else: raise RuntimeError( - f"{self._name} device failure:" f" {e.args[0].desc}" - ) + f"{self._name} device failure:" f" {de.args[0].desc}" + ) from de return AsyncStatus(wait_for_reply(rid, timeout)) @@ -461,9 +465,9 @@ def get_dtype_extended(datatype): def get_trl_descriptor( - datatype: Optional[Type], + datatype: type | None, tango_resource: str, - tr_configs: Dict[str, Union[AttributeInfoEx, CommandInfo]], + tr_configs: dict[str, AttributeInfoEx | CommandInfo], ) -> dict: tr_dtype = {} for tr_name, config in tr_configs.items(): @@ -524,7 +528,7 @@ def get_trl_descriptor( trl_choices = list(DevState.names.keys()) if datatype: - if not issubclass(datatype, (Enum, DevState)): + if not issubclass(datatype, Enum | DevState): raise TypeError( f"{tango_resource} has type Enum not {datatype.__name__}" ) @@ -549,12 +553,10 @@ def get_trl_descriptor( return {"source": tango_resource, "dtype": tr_dtype_desc, "shape": []} -async def get_tango_trl( - full_trl: str, device_proxy: Optional[DeviceProxy] -) -> TangoProxy: +async def get_tango_trl(full_trl: str, device_proxy: DeviceProxy | None) -> TangoProxy: device_trl, trl_name = full_trl.rsplit("/", 1) trl_name = trl_name.lower() - device_proxy = device_proxy or await DeviceProxy(device_trl) + device_proxy = device_proxy or await AsyncDeviceProxy(device_trl) # all attributes can be always accessible with low register all_attrs = [attr_name.lower() for attr_name in device_proxy.get_attribute_list()] @@ -579,29 +581,29 @@ async def get_tango_trl( class TangoSignalBackend(SignalBackend[T]): def __init__( self, - datatype: Optional[Type[T]], - read_trl: Optional[str] = "", - write_trl: Optional[str] = "", - device_proxy: Optional[DeviceProxy] = None, + datatype: type[T] | None, + read_trl: str | None = "", + write_trl: str | None = "", + device_proxy: DeviceProxy | None = None, ): self.device_proxy = device_proxy self.datatype = datatype self.read_trl = read_trl self.write_trl = write_trl - self.proxies: Dict[str, TangoProxy] = { + self.proxies: dict[str, TangoProxy] = { read_trl: self.device_proxy, write_trl: self.device_proxy, } - self.trl_configs: Dict[str, AttributeInfoEx] = {} + self.trl_configs: dict[str, AttributeInfoEx] = {} self.descriptor: Descriptor = {} # type: ignore self._polling = (False, 0.1, None, 0.1) self.support_events = True self.status = None - def set_trl(self, read_trl: str, write_trl: Optional[str] = ""): + def set_trl(self, read_trl: str, write_trl: str | None = ""): self.read_trl = read_trl self.write_trl = write_trl if write_trl else read_trl - self.proxies: Dict[str, TangoProxy] = { + self.proxies: dict[str, TangoProxy] = { read_trl: self.device_proxy, write_trl: self.device_proxy, } @@ -615,8 +617,8 @@ async def _connect_and_store_config(self, trl): await self.proxies[trl].connect() self.trl_configs[trl] = await self.proxies[trl].get_config() self.proxies[trl].support_events = self.support_events - except CancelledError: - raise NotConnected(f"Could not connect to {trl}") + except CancelledError as ce: + raise NotConnected(f"Could not connect to {trl}") from ce async def connect(self, timeout: float = DEFAULT_TIMEOUT): if self.read_trl != self.write_trl: @@ -633,7 +635,7 @@ async def connect(self, timeout: float = DEFAULT_TIMEOUT): self.datatype, self.read_trl, self.trl_configs ) - async def put(self, value: Optional[T], wait=True, timeout=None): + async def put(self, value: T | None, wait=True, timeout=None): self.status = None put_status = await self.proxies[self.write_trl].put(value, wait, timeout) self.status = put_status @@ -650,7 +652,7 @@ async def get_value(self) -> T: async def get_setpoint(self) -> T: return await self.proxies[self.write_trl].get_w_value() - def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: + def set_callback(self, callback: ReadingValueCallback | None) -> None: if self.support_events is False and self._polling[0] is False: raise RuntimeError( f"Cannot set event for {self.read_trl}. " @@ -662,10 +664,14 @@ def set_callback(self, callback: Optional[ReadingValueCallback]) -> None: try: assert not self.proxies[self.read_trl].has_subscription() self.proxies[self.read_trl].subscribe_callback(callback) - except AssertionError: - raise RuntimeError("Cannot set a callback when one is already set") + except AssertionError as ae: + raise RuntimeError( + "Cannot set a callback when one" " is already set" + ) from ae except RuntimeError as exc: - raise RuntimeError(f"Cannot set callback for {self.read_trl}. {exc}") + raise RuntimeError( + f"Cannot set callback" f" for {self.read_trl}. {exc}" + ) from exc else: self.proxies[self.read_trl].unsubscribe_callback() @@ -679,5 +685,5 @@ def set_polling( ): self._polling = (allow_polling, polling_period, abs_change, rel_change) - def allow_events(self, allow: Optional[bool] = True): + def allow_events(self, allow: bool | None = True): self.support_events = allow diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index f2cc2cc168..0d78e68e22 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -1,7 +1,6 @@ import asyncio import time from enum import Enum, IntEnum -from typing import Optional, Type, Union import bluesky.plan_stubs as bps import bluesky.plans as bp @@ -182,8 +181,8 @@ class TestTangoReadable(TangoReadable): def __init__( self, - trl: Optional[str] = None, - device_proxy: Optional[Union[AsyncDeviceProxy, SyncDeviceProxy]] = None, + trl: str | None = None, + device_proxy: SyncDeviceProxy | None = None, name: str = "", ) -> None: super().__init__(trl, device_proxy, name=name) @@ -248,7 +247,7 @@ async def describe_class(fqtrl): # -------------------------------------------------------------------- -def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: +def get_test_descriptor(python_type: type[T], value: T, is_cmd: bool) -> dict: if python_type in [bool, int]: return {"dtype": "integer", "shape": []} if python_type in [float]: @@ -325,7 +324,7 @@ async def test_connect(tango_test_device): # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize("proxy", [True, False, None]) -async def test_connect_proxy(tango_test_device, proxy: Optional[bool]): +async def test_connect_proxy(tango_test_device, proxy: bool | None): if proxy is None: test_device = TestTangoReadable(trl=tango_test_device) test_device.proxy = None diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index 136600b6d9..bfc980cc64 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -3,7 +3,7 @@ import time from enum import Enum, IntEnum from random import choice -from typing import Any, Optional, Tuple, Type +from typing import Any import numpy as np import numpy.typing as npt @@ -189,7 +189,9 @@ def echo_command(self, arg): # -------------------------------------------------------------------- def assert_enum(initial_value, readout_value): if type(readout_value) in [list, tuple]: - for _initial_value, _readout_value in zip(initial_value, readout_value): + for _initial_value, _readout_value in zip( + initial_value, readout_value, strict=False + ): assert_enum(_initial_value, _readout_value) else: assert initial_value == readout_value @@ -215,7 +217,7 @@ def reset_tango_asyncio(): # -------------------------------------------------------------------- # helpers to run tests # -------------------------------------------------------------------- -def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: +def get_test_descriptor(python_type: type[T], value: T, is_cmd: bool) -> dict: if python_type in [bool, int]: return {"dtype": "integer", "shape": []} if python_type in [float]: @@ -239,10 +241,10 @@ def get_test_descriptor(python_type: Type[T], value: T, is_cmd: bool) -> dict: # -------------------------------------------------------------------- async def make_backend( - typ: Optional[Type], + typ: type | None, pv: str, connect: bool = True, - allow_events: Optional[bool] = True, + allow_events: bool | None = True, ) -> TangoSignalBackend: backend = TangoSignalBackend(typ, pv, pv) backend.allow_events(allow_events) @@ -260,7 +262,7 @@ async def prepare_device(echo_device: str, pv: str, put_value: T) -> None: # -------------------------------------------------------------------- class MonitorQueue: def __init__(self, backend: SignalBackend): - self.updates: asyncio.Queue[Tuple[Reading, Any]] = asyncio.Queue() + self.updates: asyncio.Queue[tuple[Reading, Any]] = asyncio.Queue() self.backend = backend self.subscription = backend.set_callback(self.add_reading_value) @@ -301,7 +303,7 @@ async def assert_monitor_then_put( initial_value: T, put_value: T, descriptor: dict, - datatype: Optional[Type[T]] = None, + datatype: type[T] | None = None, ): await prepare_device(echo_device, pv, initial_value) source = echo_device + "/" + pv @@ -332,7 +334,7 @@ async def test_backend_get_put_monitor_attr( pv: str, tango_type: str, d_format: AttrDataFormat, - py_type: Type[T], + py_type: type[T], initial_value: T, put_value: T, ): @@ -361,7 +363,7 @@ async def assert_put_read( pv: str, put_value: T, descriptor: dict, - datatype: Optional[Type[T]] = None, + datatype: type[T] | None = None, ): source = echo_device + "/" + pv backend = await make_backend(datatype, source) @@ -394,7 +396,7 @@ async def test_backend_get_put_monitor_cmd( pv: str, tango_type: str, d_format: AttrDataFormat, - py_type: Type[T], + py_type: type[T], initial_value: T, put_value: T, ): @@ -439,7 +441,7 @@ async def test_tango_signal_r( pv: str, tango_type: str, d_format: AttrDataFormat, - py_type: Type[T], + py_type: type[T], initial_value: T, put_value: T, use_proxy: bool, @@ -492,7 +494,7 @@ async def test_tango_signal_w( pv: str, tango_type: str, d_format: AttrDataFormat, - py_type: Type[T], + py_type: type[T], initial_value: T, put_value: T, use_proxy: bool, @@ -558,7 +560,7 @@ async def test_tango_signal_rw( pv: str, tango_type: str, d_format: AttrDataFormat, - py_type: Type[T], + py_type: type[T], initial_value: T, put_value: T, use_proxy: bool, @@ -634,7 +636,7 @@ async def test_tango_signal_auto_attrs( pv: str, tango_type: str, d_format: AttrDataFormat, - py_type: Type[T], + py_type: type[T], initial_value: T, put_value: T, use_proxy: bool, @@ -709,7 +711,7 @@ async def test_tango_signal_auto_cmds( pv: str, tango_type: str, d_format: AttrDataFormat, - py_type: Type[T], + py_type: type[T], initial_value: T, put_value: T, use_dtype: bool, From 05826eceaac80765631a37932ef8ea6151c484e7 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 19 Sep 2024 15:44:13 +0200 Subject: [PATCH 127/141] pyright fixes --- src/ophyd_async/tango/signal/_signal.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ophyd_async/tango/signal/_signal.py b/src/ophyd_async/tango/signal/_signal.py index 7698290efa..9975e9aaa6 100644 --- a/src/ophyd_async/tango/signal/_signal.py +++ b/src/ophyd_async/tango/signal/_signal.py @@ -158,7 +158,9 @@ async def tango_signal_auto( return SignalX(backend=backend, timeout=timeout, name=name) -async def infer_python_type(trl: str = "", proxy: DeviceProxy = None) -> type[T]: +async def infer_python_type( + trl: str = "", proxy: DeviceProxy | None = None +) -> object | npt.NDArray | type[DevState] | IntEnum: device_trl, tr_name = trl.rsplit("/", 1) if proxy is None: dev_proxy = await AsyncDeviceProxy(device_trl) @@ -185,7 +187,7 @@ async def infer_python_type(trl: str = "", proxy: DeviceProxy = None) -> type[T] return npt.NDArray[py_type] if isarray else py_type -async def infer_signal_character(trl, proxy: DeviceProxy = None) -> str: +async def infer_signal_character(trl, proxy: DeviceProxy | None = None) -> str: device_trl, tr_name = trl.rsplit("/", 1) if proxy is None: dev_proxy = await AsyncDeviceProxy(device_trl) @@ -218,3 +220,4 @@ async def infer_signal_character(trl, proxy: DeviceProxy = None) -> str: ) else: return "RW" + raise RuntimeError(f"Unable to infer signal character for {trl}") From 9ce96153a0937bc7db18dfddf8afffbab53dea1f Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Thu, 19 Sep 2024 15:44:51 +0200 Subject: [PATCH 128/141] pyright fixes --- .../tango/base_devices/_base_device.py | 34 +++++++++++-------- src/ophyd_async/tango/demo/_counter.py | 4 +-- src/ophyd_async/tango/demo/_detector.py | 3 +- src/ophyd_async/tango/demo/_mover.py | 4 +++ 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index db009a8fb9..c31b32e105 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -14,10 +14,7 @@ DeviceVector, Signal, ) -from ophyd_async.tango.signal import ( - make_backend, - tango_signal_auto, -) +from ophyd_async.tango.signal import TangoSignalBackend, make_backend, tango_signal_auto from tango import DeviceProxy as DeviceProxy from tango.asyncio import DeviceProxy as AsyncDeviceProxy @@ -41,7 +38,7 @@ class TangoDevice(Device): trl: str = "" proxy: DeviceProxy | None = None - _polling: tuple[bool, float, float, float] = (False, 0.1, None, 0.1) + _polling: tuple[bool, float, float | None, float | None] = (False, 0.1, None, 0.1) _signal_polling: dict[str, tuple[bool, float, float, float]] = {} _poll_only_annotated_signals: bool = True @@ -76,13 +73,14 @@ async def connect( # Set the trl of the signal backends for child in self.children(): if isinstance(child[1], Signal): - resource_name = child[0].lstrip("_") - read_trl = f"{self.trl}/{resource_name}" - child[1]._backend.set_trl(read_trl, read_trl) # noqa: SLF001 + if isinstance(child[1]._backend, TangoSignalBackend): # noqa: SLF001 + resource_name = child[0].lstrip("_") + read_trl = f"{self.trl}/{resource_name}" + child[1]._backend.set_trl(read_trl, read_trl) # noqa: SLF001 if self.proxy is not None: self.register_signals() - await fill_proxy_entries(self) + await _fill_proxy_entries(self) # set_name should be called again to propagate the new signal names self.set_name(self.name) @@ -90,15 +88,18 @@ async def connect( # Set the polling configuration if self._polling[0]: for child in self.children(): - if issubclass(type(child[1]), Signal): - child[1]._backend.set_polling(*self._polling) # noqa: SLF001 - child[1]._backend.allow_events(False) # noqa: SLF001 + child_type = type(child[1]) + if issubclass(child_type, Signal): + if isinstance(child[1]._backend, TangoSignalBackend): # noqa: SLF001 # type: ignore + child[1]._backend.set_polling(*self._polling) # noqa: SLF001 # type: ignore + child[1]._backend.allow_events(False) # noqa: SLF001 # type: ignore if self._signal_polling: for signal_name, polling in self._signal_polling.items(): if hasattr(self, signal_name): attr = getattr(self, signal_name) - attr._backend.set_polling(*polling) # noqa: SLF001 - attr._backend.allow_events(False) # noqa: SLF001 + if isinstance(attr._backend, TangoSignalBackend): # noqa: SLF001 + attr._backend.set_polling(*polling) # noqa: SLF001 + attr._backend.allow_events(False) # noqa: SLF001 await super().connect(mock=mock, timeout=timeout) @@ -195,10 +196,13 @@ def tango_create_children_from_annotations( setattr(device, name, origin(name=name, backend=backend)) elif issubclass(origin, Device) or isinstance(origin, Device): + assert callable(origin), f"{origin} is not callable." setattr(device, name, origin()) -async def fill_proxy_entries(device: TangoDevice): +async def _fill_proxy_entries(device: TangoDevice): + if device.proxy is None: + raise RuntimeError(f"Device proxy is not connected for {device.name}") proxy_trl = device.trl children = [name.lstrip("_") for name, _ in device.children()] proxy_attributes = list(device.proxy.get_attribute_list()) diff --git a/src/ophyd_async/tango/demo/_counter.py b/src/ophyd_async/tango/demo/_counter.py index 20d6bdcb74..c8903dfd6d 100644 --- a/src/ophyd_async/tango/demo/_counter.py +++ b/src/ophyd_async/tango/demo/_counter.py @@ -19,7 +19,7 @@ class TangoCounter(TangoReadable): counts: SignalR[int] sample_time: SignalRW[float] start: SignalX - reset: SignalX + _reset: SignalX def __init__(self, trl: str | None = "", name=""): super().__init__(trl, name=name) @@ -34,4 +34,4 @@ async def trigger(self) -> None: @AsyncStatus.wrap async def reset(self) -> None: - await self.reset.trigger(wait=True, timeout=DEFAULT_TIMEOUT) + await self._reset.trigger(wait=True, timeout=DEFAULT_TIMEOUT) diff --git a/src/ophyd_async/tango/demo/_detector.py b/src/ophyd_async/tango/demo/_detector.py index dc88f0c11a..dc4d8a1f35 100644 --- a/src/ophyd_async/tango/demo/_detector.py +++ b/src/ophyd_async/tango/demo/_detector.py @@ -25,7 +25,8 @@ def __init__(self, trl: str, mover_trl: str, counter_trls: list[str], name=""): self.counters[i + 1] = TangoCounter(c_trl) # Define the readables for TangoDetector - self.add_readables([self.counters, self.mover]) + # DeviceVectors are incompatible with AsyncReadable. Ignore until fixed. + self.add_readables([self.counters, self.mover]) # type: ignore def set(self, value): return self.mover.set(value) diff --git a/src/ophyd_async/tango/demo/_mover.py b/src/ophyd_async/tango/demo/_mover.py index 8705e0b57f..ce50356a55 100644 --- a/src/ophyd_async/tango/demo/_mover.py +++ b/src/ophyd_async/tango/demo/_mover.py @@ -9,6 +9,7 @@ CalculatableTimeout, ConfigSignal, HintedSignal, + SignalR, SignalRW, SignalX, WatchableAsyncStatus, @@ -27,6 +28,7 @@ class TangoMover(TangoReadable, Movable, Stoppable): # If type is None or Signal, the type will be inferred from the Tango device position: SignalRW[float] velocity: SignalRW[float] + state: SignalR[DevState] _stop: SignalX def __init__(self, trl: str | None = "", name=""): @@ -45,6 +47,8 @@ async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEO assert velocity > 0, "Motor has zero velocity" timeout = abs(value - old_position) / velocity + DEFAULT_TIMEOUT + if not (isinstance(timeout, float) or timeout is None): + raise ValueError("Timeout must be a float or None") # For this server, set returns immediately so this status should not be awaited await self.position.set(value, wait=False, timeout=timeout) From e9be82ec21d8f6cc0fceedc54447d9b3f68696f2 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Fri, 20 Sep 2024 13:35:02 +0200 Subject: [PATCH 129/141] pyright fixes --- src/ophyd_async/tango/__init__.py | 38 ++++++++++++------------- src/ophyd_async/tango/demo/_detector.py | 2 +- src/ophyd_async/tango/signal/_signal.py | 6 ++-- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py index 371a590ab5..e81278fd91 100644 --- a/src/ophyd_async/tango/__init__.py +++ b/src/ophyd_async/tango/__init__.py @@ -23,23 +23,23 @@ ) __all__ = [ - TangoDevice, - TangoReadable, - tango_polling, - TangoSignalBackend, - get_python_type, - get_dtype_extended, - get_trl_descriptor, - get_tango_trl, - infer_python_type, - infer_signal_character, - make_backend, - AttributeProxy, - CommandProxy, - ensure_proper_executor, - tango_signal_auto, - tango_signal_r, - tango_signal_rw, - tango_signal_w, - tango_signal_x, + "TangoDevice", + "TangoReadable", + "tango_polling", + "TangoSignalBackend", + "get_python_type", + "get_dtype_extended", + "get_trl_descriptor", + "get_tango_trl", + "infer_python_type", + "infer_signal_character", + "make_backend", + "AttributeProxy", + "CommandProxy", + "ensure_proper_executor", + "tango_signal_auto", + "tango_signal_r", + "tango_signal_rw", + "tango_signal_w", + "tango_signal_x", ] diff --git a/src/ophyd_async/tango/demo/_detector.py b/src/ophyd_async/tango/demo/_detector.py index dc4d8a1f35..fc53e48dcf 100644 --- a/src/ophyd_async/tango/demo/_detector.py +++ b/src/ophyd_async/tango/demo/_detector.py @@ -11,7 +11,7 @@ class TangoDetector(TangoReadable): - counters: DeviceVector + counters: DeviceVector[TangoCounter] mover: TangoMover def __init__(self, trl: str, mover_trl: str, counter_trls: list[str], name=""): diff --git a/src/ophyd_async/tango/signal/_signal.py b/src/ophyd_async/tango/signal/_signal.py index 9975e9aaa6..cb03f15d59 100644 --- a/src/ophyd_async/tango/signal/_signal.py +++ b/src/ophyd_async/tango/signal/_signal.py @@ -17,8 +17,8 @@ def make_backend( datatype: type[T] | None, - read_trl: str | None = "", - write_trl: str | None = "", + read_trl: str = "", + write_trl: str = "", device_proxy: DeviceProxy | None = None, ) -> TangoSignalBackend: return TangoSignalBackend(datatype, read_trl, write_trl, device_proxy) @@ -27,7 +27,7 @@ def make_backend( def tango_signal_rw( datatype: type[T], read_trl: str, - write_trl: str | None = None, + write_trl: str = "", device_proxy: DeviceProxy | None = None, timeout: float = DEFAULT_TIMEOUT, name: str = "", From ce3223815d31ef068292d000185b3a49d5dcc138 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Wed, 25 Sep 2024 09:46:57 +0200 Subject: [PATCH 130/141] Complying with pyright --- .../tango/signal/_tango_transport.py | 321 +++++++++++------- tests/tango/test_tango_signals.py | 5 +- 2 files changed, 200 insertions(+), 126 deletions(-) diff --git a/src/ophyd_async/tango/signal/_tango_transport.py b/src/ophyd_async/tango/signal/_tango_transport.py index e9ae18dc77..54cea4b610 100644 --- a/src/ophyd_async/tango/signal/_tango_transport.py +++ b/src/ophyd_async/tango/signal/_tango_transport.py @@ -3,10 +3,12 @@ import time from abc import abstractmethod from asyncio import CancelledError +from collections.abc import Callable, Coroutine from enum import Enum +from typing import Any, TypeVar, cast import numpy as np -from bluesky.protocols import DataKey, Descriptor, Reading +from bluesky.protocols import Descriptor, Reading from ophyd_async.core import ( DEFAULT_TIMEOUT, @@ -24,7 +26,7 @@ AttributeInfoEx, CmdArgType, CommandInfo, - DevFailed, + DevFailed, # type: ignore DeviceProxy, DevState, EventType, @@ -40,19 +42,23 @@ # time constant to wait for timeout A_BIT = 1e-5 +R = TypeVar("R") -def ensure_proper_executor(func): + +def ensure_proper_executor( + func: Callable[..., Coroutine[Any, Any, R]], +) -> Callable[..., Coroutine[Any, Any, R]]: @functools.wraps(func) - async def wrapper(self, *args, **kwargs): - current_executor = get_global_executor() - if not current_executor.in_executor_context(): + async def wrapper(self: Any, *args: Any, **kwargs: Any) -> R: + current_executor: AsyncioExecutor = get_global_executor() # type: ignore + if not current_executor.in_executor_context(): # type: ignore set_global_executor(AsyncioExecutor()) return await func(self, *args, **kwargs) - return wrapper + return cast(Callable[..., Coroutine[Any, Any, R]], wrapper) -def get_python_type(tango_type) -> tuple[bool, T, str]: +def get_python_type(tango_type: CmdArgType) -> tuple[bool, object, str]: array = is_array(tango_type) if is_int(tango_type, True): return array, int, "integer" @@ -76,28 +82,30 @@ def get_python_type(tango_type) -> tuple[bool, T, str]: class TangoProxy: - support_events = True + support_events: bool = True + _proxy: DeviceProxy + _name: str def __init__(self, device_proxy: DeviceProxy, name: str): self._proxy = device_proxy self._name = name - async def connect(self): + async def connect(self) -> None: """perform actions after proxy is connected, e.g. checks if signal can be subscribed""" @abstractmethod - async def get(self) -> T: + async def get(self) -> object: """Get value from TRL""" @abstractmethod - async def get_w_value(self) -> T: + async def get_w_value(self) -> object: """Get last written value from TRL""" @abstractmethod async def put( - self, value: T | None, wait: bool = True, timeout: float | None = None - ) -> None: + self, value: object | None, wait: bool = True, timeout: float | None = None + ) -> AsyncStatus | None: """Put value to TRL""" @abstractmethod @@ -108,6 +116,7 @@ async def get_config(self) -> AttributeInfoEx | CommandInfo: async def get_reading(self) -> Reading: """Get reading from TRL""" + @abstractmethod def has_subscription(self) -> bool: """indicates, that this trl already subscribed""" @@ -131,20 +140,23 @@ def set_polling( class AttributeProxy(TangoProxy): - _callback = None - support_events = True - _eid = None - _poll_task = None - _abs_change = None - _rel_change = 0.1 - _polling_period = 0.1 - _allow_polling = False - exception = None - _last_reading = {"value": None, "timestamp": 0, "alarm_severity": 0} + _callback: ReadingValueCallback | None = None + _eid: int | None = None + _poll_task: asyncio.Task | None = None + _abs_change: float | None = None + _rel_change: float | None = 0.1 + _polling_period: float = 0.1 + _allow_polling: bool = False + exception: BaseException | None = None + _last_reading: Reading = Reading(value=None, timestamp=0, alarm_severity=0) async def connect(self) -> None: try: - eid = await self._proxy.subscribe_event( + # I have to typehint proxy as tango.DeviceProxy because + # tango.asyncio.DeviceProxy cannot be used as a typehint. + # This means pyright will not be able to see that + # subscribe_event is awaitable. + eid = await self._proxy.subscribe_event( # type: ignore self._name, EventType.CHANGE_EVENT, self._event_processor ) await self._proxy.unsubscribe_event(eid) @@ -153,19 +165,19 @@ async def connect(self) -> None: pass @ensure_proper_executor - async def get(self) -> T: + async def get(self) -> Coroutine[Any, Any, object]: attr = await self._proxy.read_attribute(self._name) return attr.value @ensure_proper_executor - async def get_w_value(self) -> T: + async def get_w_value(self) -> object: attr = await self._proxy.read_attribute(self._name) return attr.w_value @ensure_proper_executor async def put( - self, value: T | None, wait: bool = True, timeout: float | None = None - ) -> None or AsyncStatus: + self, value: object | None, wait: bool = True, timeout: float | None = None + ) -> AsyncStatus | None: if wait: try: @@ -184,16 +196,20 @@ async def _write(): else: rid = await self._proxy.write_attribute_asynch(self._name, value) - async def wait_for_reply(rd, to): - start_time = time.time() if to else None + async def wait_for_reply(rd: int, to: float | None): + start_time = time.time() while True: try: - await self._proxy.write_attribute_reply(rd) + # I have to typehint proxy as tango.DeviceProxy because + # tango.asyncio.DeviceProxy cannot be used as a typehint. + # This means pyright will not be able to see that + # write_attribute_reply is awaitable. + await self._proxy.write_attribute_reply(rd) # type: ignore break except DevFailed as exc: if exc.args[0].reason == "API_AsynReplyNotArrived": await asyncio.sleep(A_BIT) - if to and time.time() - start_time > to: + if to and (time.time() - start_time > to): raise TimeoutError( f"{self._name} attr put failed:" f" Timeout" ) from exc @@ -244,8 +260,8 @@ async def _poll(): while True: try: await self.poll() - except RuntimeError as e: - self.exception = f"Error in polling: {e}" + except RuntimeError as exc: + self.exception = exc await asyncio.sleep(1) self._poll_task = asyncio.create_task(_poll()) @@ -275,12 +291,13 @@ def unsubscribe_callback(self): def _event_processor(self, event): if not event.err: value = event.attr_value.value - reading = { - "value": value, - "timestamp": event.get_date().totime(), - "alarm_severity": event.attr_value.quality, - } - self._callback(reading, value) + reading = Reading( + value=value, + timestamp=event.get_date().totime(), + alarm_severity=event.attr_value.quality, + ) + if self._callback is not None: + self._callback(reading, value) async def poll(self): """ @@ -358,8 +375,8 @@ def set_polling( self, allow_polling: bool = False, polling_period: float = 0.5, - abs_change=None, - rel_change=0.1, + abs_change: float | None = None, + rel_change: float | None = 0.1, ): """ Set the polling parameters. @@ -371,32 +388,38 @@ def set_polling( class CommandProxy(TangoProxy): - support_events = True - _last_reading = {"value": None, "timestamp": 0, "alarm_severity": 0} + _last_reading: Reading = Reading(value=None, timestamp=0, alarm_severity=0) - async def get(self) -> T: + def subscribe_callback(self, callback: ReadingValueCallback | None) -> None: + raise NotImplementedError("Cannot subscribe to commands") + + def unsubscribe_callback(self) -> None: + raise NotImplementedError("Cannot unsubscribe from commands") + + async def get(self) -> object: return self._last_reading["value"] - async def get_w_value(self) -> T: + async def get_w_value(self) -> object: return self._last_reading["value"] + async def connect(self) -> None: + pass + @ensure_proper_executor async def put( - self, value: T | None, wait: bool = True, timeout: float | None = None - ) -> None or AsyncStatus: + self, value: object | None, wait: bool = True, timeout: float | None = None + ) -> AsyncStatus | None: if wait: try: - # val = await self._proxy.command_inout(self._name, value) + async def _put(): return await self._proxy.command_inout(self._name, value) task = asyncio.create_task(_put()) val = await asyncio.wait_for(task, timeout) - self._last_reading = { - "value": val, - "timestamp": time.time(), - "alarm_severity": 0, - } + self._last_reading = Reading( + value=val, timestamp=time.time(), alarm_severity=0 + ) except asyncio.TimeoutError as te: raise TimeoutError(f"{self._name} command failed: Timeout") from te except DevFailed as de: @@ -407,29 +430,27 @@ async def _put(): else: rid = self._proxy.command_inout_asynch(self._name, value) - async def wait_for_reply(rd, to): - reply_value = None - start_time = time.time() if to else None + async def wait_for_reply(rd: int, to: float | None): + start_time = time.time() while True: try: reply_value = self._proxy.command_inout_reply(rd) - self._last_reading = { - "value": reply_value, - "timestamp": time.time(), - "alarm_severity": 0, - } + self._last_reading = Reading( + value=reply_value, timestamp=time.time(), alarm_severity=0 + ) break - except DevFailed as de: - if de.args[0].reason == "API_AsynReplyNotArrived": + except DevFailed as de_exc: + if de_exc.args[0].reason == "API_AsynReplyNotArrived": await asyncio.sleep(A_BIT) if to and time.time() - start_time > to: raise TimeoutError( "Timeout while waiting for command reply" - ) from de + ) from de_exc else: raise RuntimeError( - f"{self._name} device failure:" f" {de.args[0].desc}" - ) from de + f"{self._name} device failure:" + f" {de_exc.args[0].desc}" + ) from de_exc return AsyncStatus(wait_for_reply(rid, timeout)) @@ -441,7 +462,7 @@ async def get_reading(self) -> Reading: reading = Reading( value=self._last_reading["value"], timestamp=self._last_reading["timestamp"], - alarm_severity=self._last_reading["alarm_severity"], + alarm_severity=self._last_reading.get("alarm_severity", 0), ) return reading @@ -449,13 +470,13 @@ def set_polling( self, allow_polling: bool = False, polling_period: float = 0.5, - abs_change=None, - rel_change=0.1, + abs_change: float | None = None, + rel_change: float | None = 0.1, ): pass -def get_dtype_extended(datatype): +def get_dtype_extended(datatype) -> object | None: # DevState tango type does not have numpy equivalents dtype = get_dtype(datatype) if dtype == np.object_: @@ -468,7 +489,7 @@ def get_trl_descriptor( datatype: type | None, tango_resource: str, tr_configs: dict[str, AttributeInfoEx | CommandInfo], -) -> dict: +) -> Descriptor: tr_dtype = {} for tr_name, config in tr_configs.items(): if isinstance(config, AttributeInfoEx): @@ -500,10 +521,18 @@ def get_trl_descriptor( # tango commands are limited in functionality: # they do not have info about shape and Enum labels trl_config = list(tr_configs.values())[0] - max_x = trl_config.max_dim_x if hasattr(trl_config, "max_dim_x") else np.Inf - max_y = trl_config.max_dim_y if hasattr(trl_config, "max_dim_y") else np.Inf - is_attr = hasattr(trl_config, "enum_labels") - trl_choices = list(trl_config.enum_labels) if is_attr else [] + max_x: int = ( + trl_config.max_dim_x + if hasattr(trl_config, "max_dim_x") + else np.iinfo(np.int32).max + ) + max_y: int = ( + trl_config.max_dim_y + if hasattr(trl_config, "max_dim_y") + else np.iinfo(np.int32).max + ) + # is_attr = hasattr(trl_config, "enum_labels") + # trl_choices = list(trl_config.enum_labels) if is_attr else [] if tr_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]: # This is an array @@ -518,48 +547,61 @@ def get_trl_descriptor( raise TypeError(f"{tango_resource} has type [{tr_dtype}] not [{dtype}]") if tr_format == AttrDataFormat.SPECTRUM: - return {"source": tango_resource, "dtype": "array", "shape": [max_x]} + return Descriptor(source=tango_resource, dtype="array", shape=[max_x]) elif tr_format == AttrDataFormat.IMAGE: - return {"source": tango_resource, "dtype": "array", "shape": [max_y, max_x]} + return Descriptor( + source=tango_resource, dtype="array", shape=[max_y, max_x] + ) else: if tr_dtype in (Enum, CmdArgType.DevState): - if tr_dtype == CmdArgType.DevState: - trl_choices = list(DevState.names.keys()) + # if tr_dtype == CmdArgType.DevState: + # trl_choices = list(DevState.names.keys()) if datatype: if not issubclass(datatype, Enum | DevState): raise TypeError( f"{tango_resource} has type Enum not {datatype.__name__}" ) - if tr_dtype == Enum and is_attr: - choices = tuple(v.name for v in datatype) - if set(choices) != set(trl_choices): - raise TypeError( - f"{tango_resource} has choices {trl_choices} not {choices}" - ) - return { - "source": tango_resource, - "dtype": "string", - "shape": [], - "choices": trl_choices, - } + # if tr_dtype == Enum and is_attr: + # if isinstance(datatype, DevState): + # choices = tuple(v.name for v in datatype) + # if set(choices) != set(trl_choices): + # raise TypeError( + # f"{tango_resource} has choices {trl_choices} " + # f"not {choices}" + # ) + return Descriptor(source=tango_resource, dtype="string", shape=[]) else: if datatype and not issubclass(tr_dtype, datatype): raise TypeError( f"{tango_resource} has type {tr_dtype.__name__} " f"not {datatype.__name__}" ) - return {"source": tango_resource, "dtype": tr_dtype_desc, "shape": []} + return Descriptor(source=tango_resource, dtype=tr_dtype_desc, shape=[]) + + raise RuntimeError(f"Error getting descriptor for {tango_resource}") -async def get_tango_trl(full_trl: str, device_proxy: DeviceProxy | None) -> TangoProxy: +async def get_tango_trl( + full_trl: str, device_proxy: DeviceProxy | TangoProxy | None +) -> TangoProxy: + if isinstance(device_proxy, TangoProxy): + return device_proxy device_trl, trl_name = full_trl.rsplit("/", 1) trl_name = trl_name.lower() - device_proxy = device_proxy or await AsyncDeviceProxy(device_trl) + if device_proxy is None: + device_proxy = await AsyncDeviceProxy(device_trl) # all attributes can be always accessible with low register - all_attrs = [attr_name.lower() for attr_name in device_proxy.get_attribute_list()] + if isinstance(device_proxy, DeviceProxy): + all_attrs = [ + attr_name.lower() for attr_name in device_proxy.get_attribute_list() + ] + else: + raise TypeError( + f"device_proxy must be an instance of DeviceProxy for {full_trl}" + ) if trl_name in all_attrs: return AttributeProxy(device_proxy, trl_name) @@ -582,28 +624,37 @@ class TangoSignalBackend(SignalBackend[T]): def __init__( self, datatype: type[T] | None, - read_trl: str | None = "", - write_trl: str | None = "", + read_trl: str = "", + write_trl: str = "", device_proxy: DeviceProxy | None = None, ): self.device_proxy = device_proxy self.datatype = datatype self.read_trl = read_trl self.write_trl = write_trl - self.proxies: dict[str, TangoProxy] = { + self.proxies: dict[str, TangoProxy | DeviceProxy | None] = { read_trl: self.device_proxy, write_trl: self.device_proxy, } self.trl_configs: dict[str, AttributeInfoEx] = {} self.descriptor: Descriptor = {} # type: ignore - self._polling = (False, 0.1, None, 0.1) - self.support_events = True - self.status = None + self._polling: tuple[bool, float, float | None, float | None] = ( + False, + 0.1, + None, + 0.1, + ) + self.support_events: bool = True + self.status: AsyncStatus | None = None + + @classmethod + def datatype_allowed(cls, dtype: Any) -> bool: + return dtype in (int, float, str, bool, np.ndarray, Enum, DevState) - def set_trl(self, read_trl: str, write_trl: str | None = ""): + def set_trl(self, read_trl: str = "", write_trl: str = ""): self.read_trl = read_trl self.write_trl = write_trl if write_trl else read_trl - self.proxies: dict[str, TangoProxy] = { + self.proxies = { read_trl: self.device_proxy, write_trl: self.device_proxy, } @@ -611,16 +662,24 @@ def set_trl(self, read_trl: str, write_trl: str | None = ""): def source(self, name: str) -> str: return self.read_trl - async def _connect_and_store_config(self, trl): + async def _connect_and_store_config(self, trl: str) -> None: + if not trl: + raise RuntimeError(f"trl not set for {self}") try: self.proxies[trl] = await get_tango_trl(trl, self.proxies[trl]) - await self.proxies[trl].connect() - self.trl_configs[trl] = await self.proxies[trl].get_config() - self.proxies[trl].support_events = self.support_events + if self.proxies[trl] is None: + raise NotConnected(f"Not connected to {trl}") + # Pyright does not believe that self.proxies[trl] is not None despite + # the check above + await self.proxies[trl].connect() # type: ignore + self.trl_configs[trl] = await self.proxies[trl].get_config() # type: ignore + self.proxies[trl].support_events = self.support_events # type: ignore except CancelledError as ce: raise NotConnected(f"Could not connect to {trl}") from ce - async def connect(self, timeout: float = DEFAULT_TIMEOUT): + async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None: + if not self.read_trl: + raise RuntimeError(f"trl not set for {self}") if self.read_trl != self.write_trl: # Different, need to connect both await wait_for_connection( @@ -630,29 +689,45 @@ async def connect(self, timeout: float = DEFAULT_TIMEOUT): else: # The same, so only need to connect one await self._connect_and_store_config(self.read_trl) - self.proxies[self.read_trl].set_polling(*self._polling) + self.proxies[self.read_trl].set_polling(*self._polling) # type: ignore self.descriptor = get_trl_descriptor( self.datatype, self.read_trl, self.trl_configs ) - async def put(self, value: T | None, wait=True, timeout=None): + async def put(self, value: T | None, wait=True, timeout=None) -> None: + if self.proxies[self.write_trl] is None: + raise NotConnected(f"Not connected to {self.write_trl}") self.status = None - put_status = await self.proxies[self.write_trl].put(value, wait, timeout) + put_status = await self.proxies[self.write_trl].put(value, wait, timeout) # type: ignore self.status = put_status - async def get_datakey(self, source: str) -> DataKey: + async def get_datakey(self, source: str) -> Descriptor: return self.descriptor async def get_reading(self) -> Reading: - return await self.proxies[self.read_trl].get_reading() + if self.proxies[self.read_trl] is None: + raise NotConnected(f"Not connected to {self.read_trl}") + return await self.proxies[self.read_trl].get_reading() # type: ignore async def get_value(self) -> T: - return await self.proxies[self.write_trl].get() + if self.proxies[self.read_trl] is None: + raise NotConnected(f"Not connected to {self.read_trl}") + proxy = self.proxies[self.read_trl] + if proxy is None: + raise NotConnected(f"Not connected to {self.read_trl}") + return cast(T, await proxy.get()) async def get_setpoint(self) -> T: - return await self.proxies[self.write_trl].get_w_value() + if self.proxies[self.write_trl] is None: + raise NotConnected(f"Not connected to {self.write_trl}") + proxy = self.proxies[self.write_trl] + if proxy is None: + raise NotConnected(f"Not connected to {self.write_trl}") + return cast(T, await proxy.get_w_value()) def set_callback(self, callback: ReadingValueCallback | None) -> None: + if self.proxies[self.read_trl] is None: + raise NotConnected(f"Not connected to {self.read_trl}") if self.support_events is False and self._polling[0] is False: raise RuntimeError( f"Cannot set event for {self.read_trl}. " @@ -662,8 +737,8 @@ def set_callback(self, callback: ReadingValueCallback | None) -> None: if callback: try: - assert not self.proxies[self.read_trl].has_subscription() - self.proxies[self.read_trl].subscribe_callback(callback) + assert not self.proxies[self.read_trl].has_subscription() # type: ignore + self.proxies[self.read_trl].subscribe_callback(callback) # type: ignore except AssertionError as ae: raise RuntimeError( "Cannot set a callback when one" " is already set" @@ -674,16 +749,16 @@ def set_callback(self, callback: ReadingValueCallback | None) -> None: ) from exc else: - self.proxies[self.read_trl].unsubscribe_callback() + self.proxies[self.read_trl].unsubscribe_callback() # type: ignore def set_polling( self, allow_polling: bool = True, polling_period: float = 0.1, - abs_change=None, - rel_change=0.1, + abs_change: float | None = None, + rel_change: float | None = 0.1, ): self._polling = (allow_polling, polling_period, abs_change, rel_change) - def allow_events(self, allow: bool | None = True): + def allow_events(self, allow: bool = True): self.support_events = allow diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index bfc980cc64..330e680a09 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -225,17 +225,16 @@ def get_test_descriptor(python_type: type[T], value: T, is_cmd: bool) -> dict: if python_type in [str]: return {"dtype": "string", "shape": []} if issubclass(python_type, DevState): - return {"dtype": "string", "shape": [], "choices": list(DevState.names.keys())} + return {"dtype": "string", "shape": []} if issubclass(python_type, Enum): return { "dtype": "string", "shape": [], - "choices": [] if is_cmd else [member.name for member in value.__class__], } return { "dtype": "array", - "shape": [np.Inf] if is_cmd else list(np.array(value).shape), + "shape": [np.iinfo(np.int32).max] if is_cmd else list(np.array(value).shape), } From 329179cc4f09e36d5c21b8d2a0affbf3bcaff830 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 30 Sep 2024 10:24:45 +0200 Subject: [PATCH 131/141] Trigger CI From 4bb7853455ee23b9bd3f91935b18811a01528e04 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 30 Sep 2024 11:22:07 +0200 Subject: [PATCH 132/141] Fixing tests and docs --- tests/tango/test_tango_transport.py | 135 +++++++++++++++++----------- 1 file changed, 84 insertions(+), 51 deletions(-) diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index b21c041bd5..05b4be26a0 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -58,26 +58,6 @@ def reset_tango_asyncio(): set_global_executor(None) -# -------------------------------------------------------------------- -@pytest.fixture(scope="module") -async def device_proxy(tango_test_device): - return await DeviceProxy(tango_test_device) - - -# -------------------------------------------------------------------- -@pytest.fixture(scope="module") -async def device_proxy_asynch(tango_test_device_asynch): - return await DeviceProxy(tango_test_device_asynch) - - -# -------------------------------------------------------------------- -@pytest.fixture(scope="module") -async def transport(echo_device): - await prepare_device(echo_device, "float_scalar_attr", 1.0) - source = echo_device + "/" + "float_scalar_attr" - return await make_backend(float, source, connect=True) - - # -------------------------------------------------------------------- class HelperClass: @ensure_proper_executor @@ -235,7 +215,8 @@ async def test_get_tango_trl( @pytest.mark.asyncio @pytest.mark.parametrize("attr", ["justvalue", "array"]) -async def test_attribute_proxy_get(device_proxy, attr): +async def test_attribute_proxy_get(tango_test_device, attr): + device_proxy = await DeviceProxy(tango_test_device) attr_proxy = AttributeProxy(device_proxy, attr) val = None val = await attr_proxy.get() @@ -248,7 +229,8 @@ async def test_attribute_proxy_get(device_proxy, attr): "attr, wait", [("justvalue", True), ("justvalue", False), ("array", True), ("array", False)], ) -async def test_attribute_proxy_put(device_proxy, attr, wait): +async def test_attribute_proxy_put(tango_test_device, attr, wait): + device_proxy = await DeviceProxy(tango_test_device) attr_proxy = AttributeProxy(device_proxy, attr) old_value = await attr_proxy.get() @@ -269,7 +251,8 @@ async def test_attribute_proxy_put(device_proxy, attr, wait): # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize("wait", [True, False]) -async def test_attribute_proxy_put_force_timeout(device_proxy, wait): +async def test_attribute_proxy_put_force_timeout(tango_test_device, wait): + device_proxy = await DeviceProxy(tango_test_device) attr_proxy = AttributeProxy(device_proxy, "slow_attribute") with pytest.raises(TimeoutError) as exc_info: status = await attr_proxy.put(3.0, wait=wait, timeout=0.1) @@ -280,7 +263,8 @@ async def test_attribute_proxy_put_force_timeout(device_proxy, wait): # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize("wait", [True, False]) -async def test_attribute_proxy_put_exceptions(device_proxy, wait): +async def test_attribute_proxy_put_exceptions(tango_test_device, wait): + device_proxy = await DeviceProxy(tango_test_device) attr_proxy = AttributeProxy(device_proxy, "raise_exception_attr") with pytest.raises(RuntimeError) as exc_info: status = await attr_proxy.put(3.0, wait=wait) @@ -293,7 +277,8 @@ async def test_attribute_proxy_put_exceptions(device_proxy, wait): @pytest.mark.parametrize( "attr, new_value", [("justvalue", 10), ("array", np.array([[2, 3, 4], [5, 6, 7]]))] ) -async def test_attribute_proxy_get_w_value(device_proxy, attr, new_value): +async def test_attribute_proxy_get_w_value(tango_test_device, attr, new_value): + device_proxy = await DeviceProxy(tango_test_device) attr_proxy = AttributeProxy(device_proxy, attr) await attr_proxy.put(new_value) attr_proxy_value = await attr_proxy.get() @@ -305,7 +290,8 @@ async def test_attribute_proxy_get_w_value(device_proxy, attr, new_value): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_attribute_get_config(device_proxy): +async def test_attribute_get_config(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) attr_proxy = AttributeProxy(device_proxy, "justvalue") config = await attr_proxy.get_config() assert config.writable is not None @@ -313,14 +299,17 @@ async def test_attribute_get_config(device_proxy): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_attribute_get_reading(device_proxy): +async def test_attribute_get_reading(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) attr_proxy = AttributeProxy(device_proxy, "justvalue") reading = await attr_proxy.get_reading() assert reading["value"] is not None # -------------------------------------------------------------------- -def test_attribute_has_subscription(device_proxy): +@pytest.mark.asyncio +async def test_attribute_has_subscription(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) attr_proxy = AttributeProxy(device_proxy, "justvalue") expected = bool(attr_proxy._callback) has_subscription = attr_proxy.has_subscription() @@ -374,7 +363,9 @@ def callback(reading, value): # -------------------------------------------------------------------- -def test_attribute_set_polling(device_proxy): +@pytest.mark.asyncio +async def test_attribute_set_polling(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) attr_proxy = AttributeProxy(device_proxy, "justvalue") attr_proxy.set_polling(True, 0.1, 1, 0.1) assert attr_proxy._allow_polling @@ -386,7 +377,8 @@ def test_attribute_set_polling(device_proxy): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_attribute_poll(device_proxy): +async def test_attribute_poll(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) attr_proxy = AttributeProxy(device_proxy, "floatvalue") attr_proxy.support_events = False @@ -445,7 +437,8 @@ def bad_callback(): # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize("attr", ["array", "label"]) -async def test_attribute_poll_stringsandarrays(device_proxy, attr): +async def test_attribute_poll_stringsandarrays(tango_test_device, attr): + device_proxy = await DeviceProxy(tango_test_device) attr_proxy = AttributeProxy(device_proxy, attr) attr_proxy.support_events = False @@ -470,7 +463,8 @@ def callback(reading, value): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_attribute_poll_exceptions(device_proxy): +async def test_attribute_poll_exceptions(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) # Try to poll a non-existent attribute attr_proxy = AttributeProxy(device_proxy, "nonexistent") attr_proxy.support_events = False @@ -486,7 +480,8 @@ def callback(reading, value): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_command_proxy_put_wait(device_proxy): +async def test_command_proxy_put_wait(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) cmd_proxy = CommandProxy(device_proxy, "clear") cmd_proxy._last_reading = None @@ -503,7 +498,8 @@ async def test_command_proxy_put_wait(device_proxy): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_command_proxy_put_nowait(device_proxy): +async def test_command_proxy_put_nowait(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) cmd_proxy = CommandProxy(device_proxy, "slow_command") # Reply before timeout @@ -531,7 +527,8 @@ async def test_command_proxy_put_nowait(device_proxy): # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize("wait", [True, False]) -async def test_command_proxy_put_exceptions(device_proxy, wait): +async def test_command_proxy_put_exceptions(tango_test_device, wait): + device_proxy = await DeviceProxy(tango_test_device) cmd_proxy = CommandProxy(device_proxy, "raise_exception_cmd") with pytest.raises(RuntimeError) as exc_info: await cmd_proxy.put(None, wait=True) @@ -540,7 +537,8 @@ async def test_command_proxy_put_exceptions(device_proxy, wait): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_command_get(device_proxy): +async def test_command_get(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) cmd_proxy = CommandProxy(device_proxy, "clear") await cmd_proxy.put(None, wait=True, timeout=1.0) reading = cmd_proxy._last_reading @@ -549,7 +547,8 @@ async def test_command_get(device_proxy): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_command_get_config(device_proxy): +async def test_command_get_config(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) cmd_proxy = CommandProxy(device_proxy, "clear") config = await cmd_proxy.get_config() assert config.out_type is not None @@ -557,7 +556,8 @@ async def test_command_get_config(device_proxy): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_command_get_reading(device_proxy): +async def test_command_get_reading(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) cmd_proxy = CommandProxy(device_proxy, "clear") await cmd_proxy.put(None, wait=True, timeout=1.0) reading = await cmd_proxy.get_reading() @@ -565,7 +565,9 @@ async def test_command_get_reading(device_proxy): # -------------------------------------------------------------------- -def test_command_set_polling(device_proxy): +@pytest.mark.asyncio +async def test_command_set_polling(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) cmd_proxy = CommandProxy(device_proxy, "clear") cmd_proxy.set_polling(True, 0.1) # Set polling in the command proxy currently does nothing @@ -612,7 +614,10 @@ async def test_tango_transport_connect_and_store_config(echo_device): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_tango_transport_put(transport): +async def test_tango_transport_put(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + transport = await make_backend(float, source, connect=True) source = transport.source("") await transport.put(2.0) val = await transport.proxies[source].get_w_value() @@ -621,8 +626,11 @@ async def test_tango_transport_put(transport): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_tango_transport_get_datakey(transport): - source = transport.source("") +async def test_tango_transport_get_datakey(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + transport = await make_backend(float, source, connect=False) + await transport.connect() datakey = await transport.get_datakey(source) assert datakey["source"] == source assert datakey["dtype"] == "number" @@ -631,21 +639,33 @@ async def test_tango_transport_get_datakey(transport): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_tango_transport_get_reading(transport): +async def test_tango_transport_get_reading(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + transport = await make_backend(float, source, connect=False) + await transport.connect() reading = await transport.get_reading() assert reading["value"] == 1.0 # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_tango_transport_get_value(transport): +async def test_tango_transport_get_value(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + transport = await make_backend(float, source, connect=False) + await transport.connect() value = await transport.get_value() assert value == 1.0 # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_tango_transport_get_setpoint(transport): +async def test_tango_transport_get_setpoint(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + transport = await make_backend(float, source, connect=False) + await transport.connect() new_setpoint = 2.0 await transport.put(new_setpoint) setpoint = await transport.get_setpoint() @@ -654,7 +674,11 @@ async def test_tango_transport_get_setpoint(transport): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_set_callback(transport): +async def test_set_callback(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + transport = await make_backend(float, source, connect=False) + await transport.connect() val = None def callback(reading, value): @@ -692,7 +716,10 @@ def callback(reading, value): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_tango_transport_set_polling(transport): +async def test_tango_transport_set_polling(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + transport = await make_backend(float, source, connect=False) transport.set_polling(True, 0.1, 1, 0.1) assert transport._polling == (True, 0.1, 1, 0.1) @@ -700,14 +727,18 @@ async def test_tango_transport_set_polling(transport): # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize("allow", [True, False]) -async def test_tango_transport_allow_events(transport, allow): +async def test_tango_transport_allow_events(echo_device, allow): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + transport = await make_backend(float, source, connect=False) transport.allow_events(allow) assert transport.support_events == allow # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_tango_transport_read_and_write_trl(device_proxy): +async def test_tango_transport_read_and_write_trl(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) trl = device_proxy.dev_name() read_trl = trl + "/" + "readback" write_trl = trl + "/" + "setpoint" @@ -735,7 +766,8 @@ async def test_tango_transport_read_and_write_trl(device_proxy): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_tango_transport_read_only_trl(device_proxy): +async def test_tango_transport_read_only_trl(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) trl = device_proxy.dev_name() read_trl = trl + "/" + "readonly" @@ -749,7 +781,8 @@ async def test_tango_transport_read_only_trl(device_proxy): # -------------------------------------------------------------------- @pytest.mark.asyncio -async def test_tango_transport_nonexistent_trl(device_proxy): +async def test_tango_transport_nonexistent_trl(tango_test_device): + device_proxy = await DeviceProxy(tango_test_device) trl = device_proxy.dev_name() nonexistent_trl = trl + "/" + "nonexistent" From 8f121dc362ba5d5f1ca12e89afa289a54de26a36 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 30 Sep 2024 11:23:28 +0200 Subject: [PATCH 133/141] Fixing tests and docs --- .../tango/base_devices/_base_device.py | 18 ++---------------- .../tango/base_devices/_tango_readable.py | 8 ++++---- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index c31b32e105..79c6a1e825 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -123,8 +123,8 @@ def tango_polling( Parameters ---------- - polling : Optional[Union[Tuple[float, float, float], - Dict[str, Tuple[float, float, float]]]], optional + polling : Optional[Union[Tuple[float, float, float], Dict[str, Tuple[float, float, + float]]]], optional Device-level polling configuration as a tuple of three floats representing the polling interval, polling timeout, and polling delay. Alternatively, a dictionary can be provided to specify signal-level polling configurations @@ -139,20 +139,6 @@ def tango_polling( Callable A class decorator that sets the `_polling` and `_signal_polling` attributes on the decorated class. - - Example - ------- - Device-level and signal-level polling: - @tango_polling( - polling=(0.5, 1.0, 0.1), - signal_polling={ - 'signal1': (0.5, 1.0, 0.1), - 'signal2': (1.0, 2.0, 0.2), - } - ) - class MyTangoDevice(TangoDevice): - signal1: Signal - signal2: Signal """ if isinstance(polling, dict): signal_polling = polling diff --git a/src/ophyd_async/tango/base_devices/_tango_readable.py b/src/ophyd_async/tango/base_devices/_tango_readable.py index 44bfc93888..f9011b6800 100644 --- a/src/ophyd_async/tango/base_devices/_tango_readable.py +++ b/src/ophyd_async/tango/base_devices/_tango_readable.py @@ -15,10 +15,10 @@ class TangoReadable(TangoDevice, StandardReadable): Usage: to proper signals mount should be awaited: new_device = await TangoDevice() - attributes: - trl: Tango resource locator, typically of the device server. - proxy: AsyncDeviceProxy object for the device. This is created when the - device is connected. + Attributes: + trl: Tango resource locator, typically of the device server. + proxy: AsyncDeviceProxy object for the device. This is created when the + device is connected. """ def __init__( From e022d1179bf3abc67c67aac9f505c13eee49a673 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 30 Sep 2024 13:21:00 +0200 Subject: [PATCH 134/141] Fixed indentation for docs --- src/ophyd_async/tango/base_devices/_base_device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index 79c6a1e825..f462d38969 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -123,8 +123,8 @@ def tango_polling( Parameters ---------- - polling : Optional[Union[Tuple[float, float, float], Dict[str, Tuple[float, float, - float]]]], optional + polling : Optional[Union[Tuple[float, float, float], + Dict[str, Tuple[float, float, float]]]], optional Device-level polling configuration as a tuple of three floats representing the polling interval, polling timeout, and polling delay. Alternatively, a dictionary can be provided to specify signal-level polling configurations From 4238fb3e74618bf9ae104cbe30b7fdae75e73dbf Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 30 Sep 2024 13:26:01 +0200 Subject: [PATCH 135/141] Fixing indentation --- src/ophyd_async/tango/base_devices/_tango_readable.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/_tango_readable.py b/src/ophyd_async/tango/base_devices/_tango_readable.py index f9011b6800..4a8fe3d1a4 100644 --- a/src/ophyd_async/tango/base_devices/_tango_readable.py +++ b/src/ophyd_async/tango/base_devices/_tango_readable.py @@ -15,10 +15,13 @@ class TangoReadable(TangoDevice, StandardReadable): Usage: to proper signals mount should be awaited: new_device = await TangoDevice() - Attributes: - trl: Tango resource locator, typically of the device server. - proxy: AsyncDeviceProxy object for the device. This is created when the - device is connected. + Attributes + ---------- + trl : str + Tango resource locator, typically of the device server. + proxy : AsyncDeviceProxy + AsyncDeviceProxy object for the device. This is created when the + device is connected. """ def __init__( From ea1b6370dbede4d4890bb654d8e659f75874de9e Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 30 Sep 2024 13:43:34 +0200 Subject: [PATCH 136/141] Fixing docs --- src/ophyd_async/tango/base_devices/_base_device.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index f462d38969..5e531709e5 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -133,12 +133,6 @@ def tango_polling( Signal-level polling configuration as a dictionary where keys are signal names and values are tuples of three floats representing the polling interval, polling timeout, and polling delay. - - Returns - ------- - Callable - A class decorator that sets the `_polling` and `_signal_polling` attributes on - the decorated class. """ if isinstance(polling, dict): signal_polling = polling From c029407011a60b52c7990d2c66d2890316d9a287 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 30 Sep 2024 13:47:33 +0200 Subject: [PATCH 137/141] Fixing docs --- src/ophyd_async/tango/signal/_signal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ophyd_async/tango/signal/_signal.py b/src/ophyd_async/tango/signal/_signal.py index cb03f15d59..b4d9f92bca 100644 --- a/src/ophyd_async/tango/signal/_signal.py +++ b/src/ophyd_async/tango/signal/_signal.py @@ -86,7 +86,7 @@ def tango_signal_w( timeout: float = DEFAULT_TIMEOUT, name: str = "", ) -> SignalW[T]: - """Create a `TangoSignalW` backed by 1 Tango Attribute/Command + """Create a `SignalW` backed by 1 Tango Attribute/Command Parameters ---------- From 632eccc6a7e3b934e59de7b5f21a2d63dfa96eaa Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 30 Sep 2024 16:23:19 +0200 Subject: [PATCH 138/141] improving coverage --- tests/tango/test_base_device.py | 23 +++++++++++ tests/tango/test_tango_transport.py | 59 ++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index 0d78e68e22..c0b79d425e 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -321,6 +321,27 @@ async def test_connect(tango_test_device): compare_values(values, await test_device.read()) +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_set_trl(tango_test_device): + values, description = await describe_class(tango_test_device) + + # async with DeviceCollector(): + # test_device = TestTangoReadable(trl=tango_test_device) + test_device = TestTangoReadable(name="test_device") + + with pytest.raises(ValueError) as excinfo: + test_device.set_trl(0) + assert "TRL must be a string." in str(excinfo.value) + + test_device.set_trl(tango_test_device) + await test_device.connect() + + assert test_device.name == "test_device" + assert description == await test_device.describe() + compare_values(values, await test_device.read()) + + # -------------------------------------------------------------------- @pytest.mark.asyncio @pytest.mark.parametrize("proxy", [True, False, None]) @@ -362,6 +383,8 @@ async def test_tango_demo(demo_test_context): counter_trls=["demo/counter/1", "demo/counter/2"], ) await detector.connect() + await detector.trigger() + await detector.mover.velocity.set(0.5) RE = RunEngine() diff --git a/tests/tango/test_tango_transport.py b/tests/tango/test_tango_transport.py index 05b4be26a0..4e951ba80e 100644 --- a/tests/tango/test_tango_transport.py +++ b/tests/tango/test_tango_transport.py @@ -11,6 +11,9 @@ prepare_device, ) +from ophyd_async.core import ( + NotConnected, +) from ophyd_async.tango import ( AttributeProxy, CommandProxy, @@ -593,13 +596,35 @@ async def test_tango_transport_source(echo_device): assert transport_source == source +# -------------------------------------------------------------------- +@pytest.mark.asyncio +async def test_tango_transport_datatype_allowed(echo_device): + await prepare_device(echo_device, "float_scalar_attr", 1.0) + source = echo_device + "/" + "float_scalar_attr" + backend = await make_backend(float, source) + + assert backend.datatype_allowed(int) + assert backend.datatype_allowed(float) + assert backend.datatype_allowed(str) + assert backend.datatype_allowed(bool) + assert backend.datatype_allowed(np.ndarray) + assert backend.datatype_allowed(Enum) + assert backend.datatype_allowed(DevState) + assert not backend.datatype_allowed(list) + + # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_tango_transport_connect(echo_device): await prepare_device(echo_device, "float_scalar_attr", 1.0) source = echo_device + "/" + "float_scalar_attr" - backend = await make_backend(float, source, connect=True) + backend = await make_backend(float, source, connect=False) assert backend is not None + await backend.connect() + backend.read_trl = "" + with pytest.raises(RuntimeError) as exc_info: + await backend.connect() + assert "trl not set" in str(exc_info.value) # -------------------------------------------------------------------- @@ -611,13 +636,23 @@ async def test_tango_transport_connect_and_store_config(echo_device): await transport._connect_and_store_config(source) assert transport.trl_configs[source] is not None + with pytest.raises(RuntimeError) as exc_info: + await transport._connect_and_store_config("") + assert "trl not set" in str(exc_info.value) + # -------------------------------------------------------------------- @pytest.mark.asyncio async def test_tango_transport_put(echo_device): await prepare_device(echo_device, "float_scalar_attr", 1.0) source = echo_device + "/" + "float_scalar_attr" - transport = await make_backend(float, source, connect=True) + transport = await make_backend(float, source, connect=False) + + with pytest.raises(NotConnected) as exc_info: + await transport.put(1.0) + assert "Not connected" in str(exc_info.value) + + await transport.connect() source = transport.source("") await transport.put(2.0) val = await transport.proxies[source].get_w_value() @@ -643,6 +678,11 @@ async def test_tango_transport_get_reading(echo_device): await prepare_device(echo_device, "float_scalar_attr", 1.0) source = echo_device + "/" + "float_scalar_attr" transport = await make_backend(float, source, connect=False) + + with pytest.raises(NotConnected) as exc_info: + await transport.put(1.0) + assert "Not connected" in str(exc_info.value) + await transport.connect() reading = await transport.get_reading() assert reading["value"] == 1.0 @@ -654,6 +694,11 @@ async def test_tango_transport_get_value(echo_device): await prepare_device(echo_device, "float_scalar_attr", 1.0) source = echo_device + "/" + "float_scalar_attr" transport = await make_backend(float, source, connect=False) + + with pytest.raises(NotConnected) as exc_info: + await transport.put(1.0) + assert "Not connected" in str(exc_info.value) + await transport.connect() value = await transport.get_value() assert value == 1.0 @@ -665,6 +710,11 @@ async def test_tango_transport_get_setpoint(echo_device): await prepare_device(echo_device, "float_scalar_attr", 1.0) source = echo_device + "/" + "float_scalar_attr" transport = await make_backend(float, source, connect=False) + + with pytest.raises(NotConnected) as exc_info: + await transport.put(1.0) + assert "Not connected" in str(exc_info.value) + await transport.connect() new_setpoint = 2.0 await transport.put(new_setpoint) @@ -678,6 +728,11 @@ async def test_set_callback(echo_device): await prepare_device(echo_device, "float_scalar_attr", 1.0) source = echo_device + "/" + "float_scalar_attr" transport = await make_backend(float, source, connect=False) + + with pytest.raises(NotConnected) as exc_info: + await transport.put(1.0) + assert "Not connected" in str(exc_info.value) + await transport.connect() val = None From b05024e1fcee03317afb3957e2eb0c2e62858a74 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Mon, 7 Oct 2024 11:19:53 +0200 Subject: [PATCH 139/141] detector demo device is now a standard readable. Tango devices can automatically infer their signals but the tango device server does not necessarily carry information about subdevices. Individual device servers should not have sub-devices but tango device databases can. This may be the subject of a future pr. Made tango_signal_auto private to discourage use. --- src/ophyd_async/tango/__init__.py | 4 ++-- .../tango/base_devices/_base_device.py | 8 +++++-- src/ophyd_async/tango/demo/_detector.py | 24 ++++++++----------- src/ophyd_async/tango/signal/__init__.py | 4 ++-- src/ophyd_async/tango/signal/_signal.py | 2 +- tests/tango/test_base_device.py | 1 - tests/tango/test_tango_signals.py | 10 ++++---- 7 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/ophyd_async/tango/__init__.py b/src/ophyd_async/tango/__init__.py index e81278fd91..5b45a067c4 100644 --- a/src/ophyd_async/tango/__init__.py +++ b/src/ophyd_async/tango/__init__.py @@ -7,6 +7,7 @@ AttributeProxy, CommandProxy, TangoSignalBackend, + __tango_signal_auto, ensure_proper_executor, get_dtype_extended, get_python_type, @@ -15,7 +16,6 @@ infer_python_type, infer_signal_character, make_backend, - tango_signal_auto, tango_signal_r, tango_signal_rw, tango_signal_w, @@ -37,7 +37,7 @@ "AttributeProxy", "CommandProxy", "ensure_proper_executor", - "tango_signal_auto", + "__tango_signal_auto", "tango_signal_r", "tango_signal_rw", "tango_signal_w", diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index 5e531709e5..4db9d27991 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -14,7 +14,11 @@ DeviceVector, Signal, ) -from ophyd_async.tango.signal import TangoSignalBackend, make_backend, tango_signal_auto +from ophyd_async.tango.signal import ( + TangoSignalBackend, + __tango_signal_auto, + make_backend, +) from tango import DeviceProxy as DeviceProxy from tango.asyncio import DeviceProxy as AsyncDeviceProxy @@ -193,7 +197,7 @@ async def _fill_proxy_entries(device: TangoDevice): if name not in children: full_trl = f"{proxy_trl}/{name}" try: - auto_signal = await tango_signal_auto( + auto_signal = await __tango_signal_auto( trl=full_trl, device_proxy=device.proxy ) setattr(device, name, auto_signal) diff --git a/src/ophyd_async/tango/demo/_detector.py b/src/ophyd_async/tango/demo/_detector.py index fc53e48dcf..61025c18c7 100644 --- a/src/ophyd_async/tango/demo/_detector.py +++ b/src/ophyd_async/tango/demo/_detector.py @@ -3,31 +3,27 @@ from ophyd_async.core import ( AsyncStatus, DeviceVector, + StandardReadable, ) -from ophyd_async.tango import TangoReadable from ._counter import TangoCounter from ._mover import TangoMover -class TangoDetector(TangoReadable): - counters: DeviceVector[TangoCounter] - mover: TangoMover - - def __init__(self, trl: str, mover_trl: str, counter_trls: list[str], name=""): - super().__init__(trl, name=name) - - # If devices are inferred from type hints, they will be created automatically - # during init. If they are created automatically, their trl must be set before - # they are connected. - self.mover.set_trl(mover_trl) - for i, c_trl in enumerate(counter_trls): - self.counters[i + 1] = TangoCounter(c_trl) +class TangoDetector(StandardReadable): + def __init__(self, mover_trl: str, counter_trls: list[str], name=""): + # A detector device may be composed of tango sub-devices + self.mover = TangoMover(mover_trl) + self.counters = DeviceVector( + {i + 1: TangoCounter(c_trl) for i, c_trl in enumerate(counter_trls)} + ) # Define the readables for TangoDetector # DeviceVectors are incompatible with AsyncReadable. Ignore until fixed. self.add_readables([self.counters, self.mover]) # type: ignore + super().__init__(name=name) + def set(self, value): return self.mover.set(value) diff --git a/src/ophyd_async/tango/signal/__init__.py b/src/ophyd_async/tango/signal/__init__.py index a285e34c03..8923718b6a 100644 --- a/src/ophyd_async/tango/signal/__init__.py +++ b/src/ophyd_async/tango/signal/__init__.py @@ -1,8 +1,8 @@ from ._signal import ( + __tango_signal_auto, infer_python_type, infer_signal_character, make_backend, - tango_signal_auto, tango_signal_r, tango_signal_rw, tango_signal_w, @@ -35,5 +35,5 @@ "tango_signal_rw", "tango_signal_w", "tango_signal_x", - "tango_signal_auto", + "__tango_signal_auto", ) diff --git a/src/ophyd_async/tango/signal/_signal.py b/src/ophyd_async/tango/signal/_signal.py index b4d9f92bca..f9274842d5 100644 --- a/src/ophyd_async/tango/signal/_signal.py +++ b/src/ophyd_async/tango/signal/_signal.py @@ -128,7 +128,7 @@ def tango_signal_x( return SignalX(backend, timeout=timeout, name=name) -async def tango_signal_auto( +async def __tango_signal_auto( datatype: type[T] | None = None, *, trl: str, diff --git a/tests/tango/test_base_device.py b/tests/tango/test_base_device.py index c0b79d425e..3c23461f99 100644 --- a/tests/tango/test_base_device.py +++ b/tests/tango/test_base_device.py @@ -377,7 +377,6 @@ async def test_with_bluesky(tango_test_device): async def test_tango_demo(demo_test_context): with demo_test_context: detector = TangoDetector( - trl="", name="detector", mover_trl="demo/motor/1", counter_trls=["demo/counter/1", "demo/counter/2"], diff --git a/tests/tango/test_tango_signals.py b/tests/tango/test_tango_signals.py index 330e680a09..40a5b591aa 100644 --- a/tests/tango/test_tango_signals.py +++ b/tests/tango/test_tango_signals.py @@ -14,7 +14,7 @@ from ophyd_async.core import SignalBackend, SignalR, SignalRW, SignalW, SignalX, T from ophyd_async.tango import ( TangoSignalBackend, - tango_signal_auto, + __tango_signal_auto, tango_signal_r, tango_signal_rw, tango_signal_w, @@ -646,7 +646,7 @@ async def test_tango_signal_auto_attrs( timeout = 0.2 async def _test_signal(dtype, proxy): - signal = await tango_signal_auto( + signal = await __tango_signal_auto( datatype=dtype, trl=source, device_proxy=proxy, @@ -720,7 +720,7 @@ async def test_tango_signal_auto_cmds( timeout = 0.2 async def _test_signal(dtype, proxy): - signal = await tango_signal_auto( + signal = await __tango_signal_auto( datatype=dtype, trl=source, device_proxy=proxy, @@ -751,7 +751,7 @@ async def _test_signal(dtype, proxy): @pytest.mark.parametrize("use_proxy", [True, False]) async def test_tango_signal_auto_cmds_void(tango_test_device: str, use_proxy: bool): proxy = await DeviceProxy(tango_test_device) if use_proxy else None - signal = await tango_signal_auto( + signal = await __tango_signal_auto( datatype=None, trl=tango_test_device + "/" + "clear", device_proxy=proxy, @@ -767,7 +767,7 @@ async def test_tango_signal_auto_cmds_void(tango_test_device: str, use_proxy: bo async def test_tango_signal_auto_badtrl(tango_test_device: str): proxy = await DeviceProxy(tango_test_device) with pytest.raises(RuntimeError) as exc_info: - await tango_signal_auto( + await __tango_signal_auto( datatype=None, trl=tango_test_device + "/" + "badtrl", device_proxy=proxy, From e289ddd8dcbffe233bbff9218247a56b343bf5f3 Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 8 Oct 2024 08:48:57 +0200 Subject: [PATCH 140/141] Adding support for the Tango control system (https://www.tango-controls.org/). This relies on PyTango (https://pypi.org/project/pytango/) ophyd-async devices to asynchronous PyTango DeviceProxy objects. The control strategy relies on a shared resource called `proxy` found in the new TangoDevice class. By passing this proxy to the TangoSignalBackend of its signals, a proxy to attributes or commands of the Tango device can be established. 1. New TangoDevice and TangoReadable device classes. 2. Automated inference of the existence unannotated signals 3. Monitoring via Tango events with optional polling of attributes. 4. Tango sensitive signals are constructed by attaching a TangoSignalBackend to a Signal object or may be built using new tango_signal_* constructor methods. 5. Signal objects with a Tango backend and Tango devices should behave the same as those with EPICS or other backends. 1. As of this commit, typed commands are not supported in Ophyd-Async so Tango command signals with a type other than None are automatically built as SignalRW as a workaround. 2. Tango commands with different input/output types are not supported. 3. Pipes are not supported. 1. Extension of Device and StandardReadable to support shared resources such as the DeviceProxy. 2. Extension of the Tango backend to support typed commands. 3. Extension of the Tango backend to support pipes. Contact: Devin Burke Research software scientist Deutsches Elektronen-Synchrotron (DESY) devin.burke@desy.de --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3865d2e4c8..7dbf22271b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ requires-python = ">=3.10" ca = ["aioca>=1.6"] pva = ["p4p"] sim = ["h5py"] -tango = ["pytango>=10.0.0rc1"] +tango = ["pytango>=10.0.0"] dev = [ "ophyd_async[pva]", "ophyd_async[sim]", From 8f8c2c3dcc8ae8121f64031db2809adddb61a55c Mon Sep 17 00:00:00 2001 From: Devin Burke Date: Tue, 8 Oct 2024 09:05:56 +0200 Subject: [PATCH 141/141] Adding support for the Tango control system (https://www.tango-controls.org/). This relies on PyTango (https://pypi.org/project/pytango/) ophyd-async devices to asynchronous PyTango DeviceProxy objects. The control strategy relies on a shared resource called `proxy` found in the new TangoDevice class. By passing this proxy to the TangoSignalBackend of its signals, a proxy to attributes or commands of the Tango device can be established. 1. New TangoDevice and TangoReadable device classes. 2. Automated inference of the existence unannotated signals 3. Monitoring via Tango events with optional polling of attributes. 4. Tango sensitive signals are constructed by attaching a TangoSignalBackend to a Signal object or may be built using new tango_signal_* constructor methods. 5. Signal objects with a Tango backend and Tango devices should behave the same as those with EPICS or other backends. 1. As of this commit, typed commands are not supported in Ophyd-Async so Tango command signals with a type other than None are automatically built as SignalRW as a workaround. 2. Tango commands with different input/output types are not supported. 3. Pipes are not supported. 1. Extension of Device and StandardReadable to support shared resources such as the DeviceProxy. 2. Extension of the Tango backend to support typed commands. 3. Extension of the Tango backend to support pipes. Contact: Devin Burke Research software scientist Deutsches Elektronen-Synchrotron (DESY) devin.burke@desy.de --- .../tango/base_devices/_base_device.py | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/ophyd_async/tango/base_devices/_base_device.py b/src/ophyd_async/tango/base_devices/_base_device.py index 4db9d27991..9d01539263 100644 --- a/src/ophyd_async/tango/base_devices/_base_device.py +++ b/src/ophyd_async/tango/base_devices/_base_device.py @@ -2,7 +2,6 @@ from typing import ( TypeVar, - Union, get_args, get_origin, get_type_hints, @@ -11,7 +10,6 @@ from ophyd_async.core import ( DEFAULT_TIMEOUT, Device, - DeviceVector, Signal, ) from ophyd_async.tango.signal import ( @@ -160,28 +158,28 @@ def tango_create_children_from_annotations( if name in ("_name", "parent"): continue - device_type, is_optional = _strip_union(device_type) - if is_optional and name not in included_optional_fields: - continue - - is_device_vector, device_type = _strip_device_vector(device_type) - if is_device_vector: - n_device_vector = DeviceVector() - setattr(device, name, n_device_vector) + # device_type, is_optional = _strip_union(device_type) + # if is_optional and name not in included_optional_fields: + # continue + # + # is_device_vector, device_type = _strip_device_vector(device_type) + # if is_device_vector: + # n_device_vector = DeviceVector() + # setattr(device, name, n_device_vector) - else: - origin = get_origin(device_type) - origin = origin if origin else device_type + # else: + origin = get_origin(device_type) + origin = origin if origin else device_type - if issubclass(origin, Signal): - type_args = get_args(device_type) - datatype = type_args[0] if type_args else None - backend = make_backend(datatype=datatype, device_proxy=device.proxy) - setattr(device, name, origin(name=name, backend=backend)) + if issubclass(origin, Signal): + type_args = get_args(device_type) + datatype = type_args[0] if type_args else None + backend = make_backend(datatype=datatype, device_proxy=device.proxy) + setattr(device, name, origin(name=name, backend=backend)) - elif issubclass(origin, Device) or isinstance(origin, Device): - assert callable(origin), f"{origin} is not callable." - setattr(device, name, origin()) + elif issubclass(origin, Device) or isinstance(origin, Device): + assert callable(origin), f"{origin} is not callable." + setattr(device, name, origin()) async def _fill_proxy_entries(device: TangoDevice): @@ -211,17 +209,17 @@ async def _fill_proxy_entries(device: TangoDevice): raise e -def _strip_union(field: T | T) -> tuple[T, bool]: - if get_origin(field) is Union: - args = get_args(field) - is_optional = type(None) in args - for arg in args: - if arg is not type(None): - return arg, is_optional - return field, False - - -def _strip_device_vector(field: type[Device]) -> tuple[bool, type[Device]]: - if get_origin(field) is DeviceVector: - return True, get_args(field)[0] - return False, field +# def _strip_union(field: T | T) -> tuple[T, bool]: +# if get_origin(field) is Union: +# args = get_args(field) +# is_optional = type(None) in args +# for arg in args: +# if arg is not type(None): +# return arg, is_optional +# return field, False +# +# +# def _strip_device_vector(field: type[Device]) -> tuple[bool, type[Device]]: +# if get_origin(field) is DeviceVector: +# return True, get_args(field)[0] +# return False, field