Skip to content

Commit

Permalink
Tango fixes and update core and tango tests to match structure of epi…
Browse files Browse the repository at this point in the history
…cs tests (#723)

Run Tango test context in subprocess to get around forking, support Tango tests on Windows

Add converters for DevState and DevEnum Tango signals

Add example one of everything Tango server providing every attribute and command type

Fix Tango trls for children provided by remote tango servers with #dbase=no

Add DevStateEnum StrictEnum for use with TangoSignals

Use MonitorQueue from ophyd_async.testing in soft, epics and tango signal tests

Remove ability to create Tango signal, backend or device from existing DeviceProxy, require trl

Make assert_reading diff output cleaner when alarm_severity or timestamp not given
  • Loading branch information
jsouter authored Mar 5, 2025
1 parent 38270ef commit f50f67d
Show file tree
Hide file tree
Showing 27 changed files with 1,412 additions and 943 deletions.
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ type = "forbidden"
forbidden_modules = ["ophyd_async.testing", "ophyd_async.sim"]
source_modules = [
"ophyd_async.plan_stubs",
"ophyd_async.fastcs",
"ophyd_async.epics",
"ophyd_async.tango",
"ophyd_async.fast.*",
"ophyd_async.epics.*",
"ophyd_async.tango.*",
]
ignore_imports = ["ophyd_async.tango.testing.* -> ophyd_async.testing"]
23 changes: 12 additions & 11 deletions src/ophyd_async/epics/testing/test_records_pva.db
Original file line number Diff line number Diff line change
@@ -1,36 +1,37 @@
record(waveform, "$(device)int8a") {
field(NELM, "3")
field(NELM, "7")
field(FTVL, "CHAR")
field(INP, {const:[-128, 127]})
field(INP, {const:[-128, 127, 0, 1, 2, 3, 4]})
field(PINI, "YES")
}

record(waveform, "$(device)uint16a") {
field(NELM, "3")
field(NELM, "7")
field(FTVL, "USHORT")
field(INP, {const:[0, 65535]})
field(INP, {const:[0, 65535, 0, 1, 2, 3, 4]})
field(PINI, "YES")
}

record(waveform, "$(device)uint32a") {
field(NELM, "3")
field(NELM, "7")
field(FTVL, "ULONG")
field(INP, {const:[0, 4294967295]})
field(INP, {const:[0, 4294967295, 0, 1, 2, 3, 4]})
field(PINI, "YES")
}

record(waveform, "$(device)int64a") {
field(NELM, "3")
field(NELM, "7")
field(FTVL, "INT64")
# Can't do 64-bit int with JSON numbers in a const link...
field(INP, {const:[-2147483649, 2147483648]})
# limit of range appears to be +/-(2^63 - 1)
field(INP, {const:[-9223372036854775807, 9223372036854775807, 0, 1, 2, 3, 4]})
field(PINI, "YES")
}

record(waveform, "$(device)uint64a") {
field(NELM, "3")
field(NELM, "7")
field(FTVL, "UINT64")
field(INP, {const:[0, 4294967297]})
# limit of range appears to be 0 to +(2^63 - 1)
field(INP, {const:[0, 9223372036854775807, 0, 1, 2, 3, 4]})
field(PINI, "YES")
}

Expand Down
4 changes: 4 additions & 0 deletions src/ophyd_async/tango/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
get_tango_trl,
get_trl_descriptor,
)
from ._utils import DevStateEnum, get_device_trl_and_attr, get_full_attr_trl

__all__ = [
"AttributeProxy",
"CommandProxy",
"DevStateEnum",
"ensure_proper_executor",
"TangoSignalBackend",
"get_python_type",
Expand All @@ -40,4 +42,6 @@
"TangoReadable",
"TangoPolling",
"TangoDeviceConnector",
"get_device_trl_and_attr",
"get_full_attr_trl",
]
29 changes: 11 additions & 18 deletions src/ophyd_async/tango/core/_base_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from tango.asyncio import DeviceProxy as AsyncDeviceProxy

from ._signal import TangoSignalBackend, infer_python_type, infer_signal_type
from ._utils import get_full_attr_trl

T = TypeVar("T")

Expand All @@ -18,9 +19,7 @@ class TangoDevice(Device):
Extends Device to provide attributes for Tango devices.
:param trl: Tango resource locator, typically of the device server.
:param device_proxy:
Asynchronous or synchronous DeviceProxy object for the device. If not
provided, an asynchronous DeviceProxy object will be created using the
An asynchronous DeviceProxy object will be created using the
trl and awaited when the device is connected.
"""

Expand All @@ -29,15 +28,13 @@ class TangoDevice(Device):

def __init__(
self,
trl: str | None = None,
device_proxy: DeviceProxy | None = None,
trl: str | None,
support_events: bool = False,
name: str = "",
auto_fill_signals: bool = True,
) -> None:
connector = TangoDeviceConnector(
trl=trl,
device_proxy=device_proxy,
support_events=support_events,
auto_fill_signals=auto_fill_signals,
)
Expand Down Expand Up @@ -75,12 +72,10 @@ class TangoDeviceConnector(DeviceConnector):
def __init__(
self,
trl: str | None,
device_proxy: DeviceProxy | None,
support_events: bool,
auto_fill_signals: bool = True,
) -> None:
self.trl = trl
self.proxy = device_proxy
self._support_events = support_events
self._auto_fill_signals = auto_fill_signals

Expand All @@ -90,7 +85,7 @@ def create_children_from_annotations(self, device: Device):
device=device,
signal_backend_factory=TangoSignalBackend,
device_connector_factory=lambda: TangoDeviceConnector(
None, None, self._support_events
None, self._support_events
),
)
list(self.filler.create_devices_from_annotations(filled=False))
Expand All @@ -108,13 +103,9 @@ async def connect_mock(self, device: Device, mock: LazyMock):
return await super().connect_mock(device, mock)

async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
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()
else:
raise TypeError("Neither proxy nor trl supplied")

if not self.trl:
raise RuntimeError(f"Could not created Device Proxy for TRL {self.trl}")
self.proxy = await AsyncDeviceProxy(self.trl)
children = sorted(
set()
.union(self.proxy.get_attribute_list())
Expand All @@ -132,11 +123,13 @@ async def connect_real(self, device: Device, timeout: float, force_reconnect: bo
for name in children:
if self._auto_fill_signals or name in not_filled:
# TODO: strip attribute name
full_trl = f"{self.trl}/{name}"
full_trl = get_full_attr_trl(self.trl, name)
signal_type = await infer_signal_type(full_trl, self.proxy)
if signal_type:
backend = self.filler.fill_child_signal(name, signal_type)
backend.datatype = await infer_python_type(full_trl, self.proxy)
# don't overlaod datatype if provided by annotation
if backend.datatype is None:
backend.datatype = await infer_python_type(full_trl, self.proxy)
backend.set_trl(full_trl)

# Check that all the requested children have been filled
Expand Down
81 changes: 81 additions & 0 deletions src/ophyd_async/tango/core/_converters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from typing import Any, Generic

import numpy as np
from numpy.typing import NDArray

from ophyd_async.core import (
SignalDatatypeT,
)
from tango import (
DevState,
)

from ._utils import DevStateEnum


class TangoConverter(Generic[SignalDatatypeT]):
def write_value(self, value: Any) -> Any:
return value

def value(self, value: Any) -> Any:
return value


class TangoEnumConverter(TangoConverter):
def __init__(self, labels: list[str]):
self._labels = labels

def write_value(self, value: str):
if not isinstance(value, str):
raise TypeError("TangoEnumConverter expects str value")
return self._labels.index(value)

def value(self, value: int):
return self._labels[value]


class TangoEnumArrayConverter(TangoConverter):
def __init__(self, labels: list[str]):
self._labels = labels

def write_value(self, value: NDArray[np.str_]) -> NDArray[np.integer]:
vfunc = np.vectorize(self._labels.index)
new_array = vfunc(value)
return new_array

def value(self, value: NDArray[np.integer]) -> NDArray[np.str_]:
vfunc = np.vectorize(self._labels.__getitem__)
new_array = vfunc(value)
return new_array


class TangoDevStateConverter(TangoConverter):
_labels = [e.value for e in DevStateEnum]

def write_value(self, value: str) -> DevState:
idx = self._labels.index(value)
return DevState(idx)

def value(self, value: DevState) -> str:
idx = int(value)
return self._labels[idx]


class TangoDevStateArrayConverter(TangoConverter):
_labels = [e.value for e in DevStateEnum]

def _write_convert(self, value):
return DevState(self._labels.index(value))

def _convert(self, value):
return self._labels[int(value)]

def write_value(self, value: NDArray[np.str_]) -> NDArray[DevState]:
vfunc = np.vectorize(self._write_convert, otypes=[DevState])
new_array = vfunc(value)
return new_array

def value(self, value: NDArray[DevState]) -> NDArray[np.str_]:
vfunc = np.vectorize(self._convert)
new_array = vfunc(value)
return new_array
28 changes: 8 additions & 20 deletions src/ophyd_async/tango/core/_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from tango.asyncio import DeviceProxy as AsyncDeviceProxy

from ._tango_transport import TangoSignalBackend, get_python_type
from ._utils import get_device_trl_and_attr

logger = logging.getLogger("ophyd_async")

Expand All @@ -35,16 +36,14 @@ def make_backend(
datatype: type[SignalDatatypeT] | None,
read_trl: str = "",
write_trl: str = "",
device_proxy: DeviceProxy | None = None,
) -> TangoSignalBackend:
return TangoSignalBackend(datatype, read_trl, write_trl, device_proxy)
return TangoSignalBackend(datatype, read_trl, write_trl)


def tango_signal_rw(
datatype: type[SignalDatatypeT],
read_trl: str,
write_trl: str = "",
device_proxy: DeviceProxy | None = None,
timeout: float = DEFAULT_TIMEOUT,
name: str = "",
) -> SignalRW[SignalDatatypeT]:
Expand All @@ -58,22 +57,19 @@ def tango_signal_rw(
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
timeout:
The timeout for the read and write operations
name:
The name of the Signal
"""
backend = make_backend(datatype, read_trl, write_trl or read_trl, device_proxy)
backend = make_backend(datatype, read_trl, write_trl or read_trl)
return SignalRW(backend, timeout=timeout, name=name)


def tango_signal_r(
datatype: type[SignalDatatypeT],
read_trl: str,
device_proxy: DeviceProxy | None = None,
timeout: float = DEFAULT_TIMEOUT,
name: str = "",
) -> SignalR[SignalDatatypeT]:
Expand All @@ -85,22 +81,19 @@ def tango_signal_r(
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
timeout:
The timeout for the read operation
name:
The name of the Signal
"""
backend = make_backend(datatype, read_trl, read_trl, device_proxy)
backend = make_backend(datatype, read_trl, read_trl)
return SignalR(backend, timeout=timeout, name=name)


def tango_signal_w(
datatype: type[SignalDatatypeT],
write_trl: str,
device_proxy: DeviceProxy | None = None,
timeout: float = DEFAULT_TIMEOUT,
name: str = "",
) -> SignalW[SignalDatatypeT]:
Expand All @@ -112,21 +105,18 @@ def tango_signal_w(
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
timeout:
The timeout for the write operation
name:
The name of the Signal
"""
backend = make_backend(datatype, write_trl, write_trl, device_proxy)
backend = make_backend(datatype, write_trl, write_trl)
return SignalW(backend, timeout=timeout, name=name)


def tango_signal_x(
write_trl: str,
device_proxy: DeviceProxy | None = None,
timeout: float = DEFAULT_TIMEOUT,
name: str = "",
) -> SignalX:
Expand All @@ -136,15 +126,13 @@ def tango_signal_x(
----------
write_trl:
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 = make_backend(None, write_trl, write_trl, device_proxy)
backend = make_backend(None, write_trl, write_trl)
return SignalX(backend, timeout=timeout, name=name)


Expand All @@ -153,7 +141,7 @@ async def infer_python_type(
) -> object | npt.NDArray | type[DevState] | IntEnum:
"""Infers the python type from the TRL."""
# TODO: work out if this is still needed
device_trl, tr_name = trl.rsplit("/", 1)
device_trl, tr_name = get_device_trl_and_attr(trl)
if proxy is None:
dev_proxy = await AsyncDeviceProxy(device_trl)
else:
Expand Down Expand Up @@ -182,7 +170,7 @@ async def infer_python_type(
async def infer_signal_type(
trl, proxy: DeviceProxy | None = None
) -> type[Signal] | None:
device_trl, tr_name = trl.rsplit("/", 1)
device_trl, tr_name = get_device_trl_and_attr(trl)
if proxy is None:
dev_proxy = await AsyncDeviceProxy(device_trl)
else:
Expand Down
Loading

0 comments on commit f50f67d

Please sign in to comment.