Source code for ophyd_async.tango.core._base_device

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Generic, TypeVar

from tango import DevFailed as TangoDevFailed
from tango import DeviceProxy
from tango.asyncio import DeviceProxy as AsyncDeviceProxy

from ophyd_async.core import (
    Command,
    Device,
    DeviceConnector,
    DeviceFiller,
    DeviceVector,
    LazyMock,
    NotConnectedError,
    Signal,
)

from ._signal import TangoSignalBackend, infer_python_type, infer_signal_type
from ._tango_command_backend import TangoCommandBackend
from ._utils import get_full_attr_trl, sig_from_types

T = TypeVar("T")


[docs] class TangoDevice(Device): """General class for TangoDevices. Extends Device to provide attributes for Tango devices. :param trl: Tango resource locator, typically of the device server. An asynchronous DeviceProxy object will be created using the trl and awaited when the device is connected. :param connector: A pre-built `DeviceConnector`, used when this device is created as a declarative sub-device of another `TangoDevice` (the parent connector builds it). Mutually exclusive with `trl`: pass one or the other, not both. When given, `support_events`/`auto_fill_signals` are ignored (they are configured on the supplied connector instead). """ _trl: str = "" def __init__( self, trl: str = "", support_events: bool = False, name: str = "", auto_fill_signals: bool = True, connector: DeviceConnector | None = None, ) -> None: if connector is not None and trl: raise ValueError("TangoDevice takes either `trl` or `connector`, not both") self._trl = trl if connector is None: connector = TangoDeviceConnector( trl=trl, support_events=support_events, auto_fill_signals=auto_fill_signals, ) super().__init__(name=name, connector=connector)
[docs] def get_trl(self) -> str: return self._trl
[docs] def get_proxy(self) -> DeviceProxy | None: connector = self._connector if isinstance(connector, TangoDeviceConnector): return connector.proxy return None
[docs] @dataclass class TangoPolling(Generic[T]): ophyd_polling_period: float = 0.1 abs_change: T | None = None rel_change: T | None = None
def fill_backend_with_polling( support_events: bool, backend: TangoSignalBackend, annotations: list[Any] ): unhandled = [] while annotations: annotation = annotations.pop(0) backend.allow_events(support_events) if isinstance(annotation, TangoPolling): backend.set_polling( not support_events, annotation.ophyd_polling_period, annotation.abs_change, annotation.rel_change, ) else: unhandled.append(annotation) annotations.extend(unhandled) # These leftover annotations will now be handled by the iterator
[docs] class TangoDeviceConnector(DeviceConnector): def __init__( self, trl: str | None, support_events: bool, auto_fill_signals: bool = True, ) -> None: self.trl = trl self.proxy: DeviceProxy | None = None self._support_events = support_events self._auto_fill_signals = auto_fill_signals
[docs] def set_trl(self, trl: str): self.trl = trl
[docs] def create_children_from_annotations(self, device: Device): if not hasattr(self, "filler"): self.filler = DeviceFiller( device=device, signal_backend_factory=TangoSignalBackend, device_connector_factory=lambda: TangoDeviceConnector( None, self._support_events ), command_backend_factory=TangoCommandBackend, ) list(self.filler.create_devices_from_annotations(filled=False)) for backend, annotations in self.filler.create_signals_from_annotations( filled=False ): fill_backend_with_polling(self._support_events, backend, annotations) list(self.filler.create_commands_from_annotations(filled=False)) self.filler.check_created()
[docs] async def connect_mock(self, device: Device, mock: LazyMock): if isinstance(device, DeviceVector): # Make 2 entries for this DeviceVector self.filler.create_device_dict_entries_to_mock([1, 2]) # Set the name of the device to name all children device.set_name(device.name) return await super().connect_mock(device, mock)
[docs] async def connect_real(self, device: Device, timeout: float, force_reconnect: bool): if not self.trl: raise RuntimeError(f"Could not created Device Proxy for TRL {self.trl}") try: proxy = await AsyncDeviceProxy(self.trl) # type: ignore except TangoDevFailed as ex: raise NotConnectedError(f"tango://{self.trl}") from ex self.proxy = proxy children = sorted( set().union(proxy.get_attribute_list()).union(proxy.get_command_list()) ) # type: ignore children = [ child for child in children if child not in self.filler.ignored_signals ] not_filled = {unfilled for unfilled, _ in device.children()} # If auto_fill_signals is True, fill all children inferred from the device # else fill only the children that are annotated for name in children: if self._auto_fill_signals or name in not_filled: # TODO: strip attribute name full_trl = get_full_attr_trl(self.trl, name) signal_type = await infer_signal_type(full_trl, proxy) if signal_type is not None and issubclass(signal_type, Signal): backend = self.filler.fill_child_signal(name, signal_type) elif signal_type is not None and issubclass(signal_type, Command): backend = self.filler.fill_child_command(name, signal_type) else: raise TypeError( f"Cannot infer type for {full_trl} (type {signal_type})" ) # don't overload datatype if provided by annotation if isinstance(backend, TangoCommandBackend): if backend.signature is None: in_type, out_type = await infer_python_type(full_trl, proxy) sig = sig_from_types(in_type, out_type) # # Pyright still thinks backend could be a # # TangoSignalBackend somehow backend.signature = sig # type: ignore elif isinstance(backend, TangoSignalBackend): if backend.datatype is None: _, out_type = await infer_python_type(full_trl, proxy) backend.datatype = out_type else: raise TypeError(f"Unknown backend type {backend}") backend.set_trl(full_trl) # Check that all the requested children have been filled self.filler.check_filled(f"{self.trl}: {children}") # Set the name of the device to name all children device.set_name(device.name) return await super().connect_real(device, timeout, force_reconnect)