Skip to content

Commit a0103a7

Browse files
jsoutershihab-dls
authored andcommitted
Tango fixes and update core and tango tests to match structure of epics 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
1 parent 8d2b40b commit a0103a7

27 files changed

+1412
-943
lines changed

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ type = "forbidden"
187187
forbidden_modules = ["ophyd_async.testing", "ophyd_async.sim"]
188188
source_modules = [
189189
"ophyd_async.plan_stubs",
190-
"ophyd_async.fastcs",
191-
"ophyd_async.epics",
192-
"ophyd_async.tango",
190+
"ophyd_async.fast.*",
191+
"ophyd_async.epics.*",
192+
"ophyd_async.tango.*",
193193
]
194+
ignore_imports = ["ophyd_async.tango.testing.* -> ophyd_async.testing"]

src/ophyd_async/epics/testing/test_records_pva.db

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,37 @@
11
record(waveform, "$(device)int8a") {
2-
field(NELM, "3")
2+
field(NELM, "7")
33
field(FTVL, "CHAR")
4-
field(INP, {const:[-128, 127]})
4+
field(INP, {const:[-128, 127, 0, 1, 2, 3, 4]})
55
field(PINI, "YES")
66
}
77

88
record(waveform, "$(device)uint16a") {
9-
field(NELM, "3")
9+
field(NELM, "7")
1010
field(FTVL, "USHORT")
11-
field(INP, {const:[0, 65535]})
11+
field(INP, {const:[0, 65535, 0, 1, 2, 3, 4]})
1212
field(PINI, "YES")
1313
}
1414

1515
record(waveform, "$(device)uint32a") {
16-
field(NELM, "3")
16+
field(NELM, "7")
1717
field(FTVL, "ULONG")
18-
field(INP, {const:[0, 4294967295]})
18+
field(INP, {const:[0, 4294967295, 0, 1, 2, 3, 4]})
1919
field(PINI, "YES")
2020
}
2121

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

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

src/ophyd_async/tango/core/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
get_tango_trl,
2020
get_trl_descriptor,
2121
)
22+
from ._utils import DevStateEnum, get_device_trl_and_attr, get_full_attr_trl
2223

2324
__all__ = [
2425
"AttributeProxy",
2526
"CommandProxy",
27+
"DevStateEnum",
2628
"ensure_proper_executor",
2729
"TangoSignalBackend",
2830
"get_python_type",
@@ -40,4 +42,6 @@
4042
"TangoReadable",
4143
"TangoPolling",
4244
"TangoDeviceConnector",
45+
"get_device_trl_and_attr",
46+
"get_full_attr_trl",
4347
]

src/ophyd_async/tango/core/_base_device.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from tango.asyncio import DeviceProxy as AsyncDeviceProxy
99

1010
from ._signal import TangoSignalBackend, infer_python_type, infer_signal_type
11+
from ._utils import get_full_attr_trl
1112

1213
T = TypeVar("T")
1314

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

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

3029
def __init__(
3130
self,
32-
trl: str | None = None,
33-
device_proxy: DeviceProxy | None = None,
31+
trl: str | None,
3432
support_events: bool = False,
3533
name: str = "",
3634
auto_fill_signals: bool = True,
3735
) -> None:
3836
connector = TangoDeviceConnector(
3937
trl=trl,
40-
device_proxy=device_proxy,
4138
support_events=support_events,
4239
auto_fill_signals=auto_fill_signals,
4340
)
@@ -75,12 +72,10 @@ class TangoDeviceConnector(DeviceConnector):
7572
def __init__(
7673
self,
7774
trl: str | None,
78-
device_proxy: DeviceProxy | None,
7975
support_events: bool,
8076
auto_fill_signals: bool = True,
8177
) -> None:
8278
self.trl = trl
83-
self.proxy = device_proxy
8479
self._support_events = support_events
8580
self._auto_fill_signals = auto_fill_signals
8681

@@ -90,7 +85,7 @@ def create_children_from_annotations(self, device: Device):
9085
device=device,
9186
signal_backend_factory=TangoSignalBackend,
9287
device_connector_factory=lambda: TangoDeviceConnector(
93-
None, None, self._support_events
88+
None, self._support_events
9489
),
9590
)
9691
list(self.filler.create_devices_from_annotations(filled=False))
@@ -108,13 +103,9 @@ async def connect_mock(self, device: Device, mock: LazyMock):
108103
return await super().connect_mock(device, mock)
109104

110105
async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
111-
if self.trl and self.proxy is None:
112-
self.proxy = await AsyncDeviceProxy(self.trl)
113-
elif self.proxy and not self.trl:
114-
self.trl = self.proxy.name()
115-
else:
116-
raise TypeError("Neither proxy nor trl supplied")
117-
106+
if not self.trl:
107+
raise RuntimeError(f"Could not created Device Proxy for TRL {self.trl}")
108+
self.proxy = await AsyncDeviceProxy(self.trl)
118109
children = sorted(
119110
set()
120111
.union(self.proxy.get_attribute_list())
@@ -132,11 +123,13 @@ async def connect_real(self, device: Device, timeout: float, force_reconnect: bo
132123
for name in children:
133124
if self._auto_fill_signals or name in not_filled:
134125
# TODO: strip attribute name
135-
full_trl = f"{self.trl}/{name}"
126+
full_trl = get_full_attr_trl(self.trl, name)
136127
signal_type = await infer_signal_type(full_trl, self.proxy)
137128
if signal_type:
138129
backend = self.filler.fill_child_signal(name, signal_type)
139-
backend.datatype = await infer_python_type(full_trl, self.proxy)
130+
# don't overlaod datatype if provided by annotation
131+
if backend.datatype is None:
132+
backend.datatype = await infer_python_type(full_trl, self.proxy)
140133
backend.set_trl(full_trl)
141134

142135
# Check that all the requested children have been filled
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from typing import Any, Generic
2+
3+
import numpy as np
4+
from numpy.typing import NDArray
5+
6+
from ophyd_async.core import (
7+
SignalDatatypeT,
8+
)
9+
from tango import (
10+
DevState,
11+
)
12+
13+
from ._utils import DevStateEnum
14+
15+
16+
class TangoConverter(Generic[SignalDatatypeT]):
17+
def write_value(self, value: Any) -> Any:
18+
return value
19+
20+
def value(self, value: Any) -> Any:
21+
return value
22+
23+
24+
class TangoEnumConverter(TangoConverter):
25+
def __init__(self, labels: list[str]):
26+
self._labels = labels
27+
28+
def write_value(self, value: str):
29+
if not isinstance(value, str):
30+
raise TypeError("TangoEnumConverter expects str value")
31+
return self._labels.index(value)
32+
33+
def value(self, value: int):
34+
return self._labels[value]
35+
36+
37+
class TangoEnumArrayConverter(TangoConverter):
38+
def __init__(self, labels: list[str]):
39+
self._labels = labels
40+
41+
def write_value(self, value: NDArray[np.str_]) -> NDArray[np.integer]:
42+
vfunc = np.vectorize(self._labels.index)
43+
new_array = vfunc(value)
44+
return new_array
45+
46+
def value(self, value: NDArray[np.integer]) -> NDArray[np.str_]:
47+
vfunc = np.vectorize(self._labels.__getitem__)
48+
new_array = vfunc(value)
49+
return new_array
50+
51+
52+
class TangoDevStateConverter(TangoConverter):
53+
_labels = [e.value for e in DevStateEnum]
54+
55+
def write_value(self, value: str) -> DevState:
56+
idx = self._labels.index(value)
57+
return DevState(idx)
58+
59+
def value(self, value: DevState) -> str:
60+
idx = int(value)
61+
return self._labels[idx]
62+
63+
64+
class TangoDevStateArrayConverter(TangoConverter):
65+
_labels = [e.value for e in DevStateEnum]
66+
67+
def _write_convert(self, value):
68+
return DevState(self._labels.index(value))
69+
70+
def _convert(self, value):
71+
return self._labels[int(value)]
72+
73+
def write_value(self, value: NDArray[np.str_]) -> NDArray[DevState]:
74+
vfunc = np.vectorize(self._write_convert, otypes=[DevState])
75+
new_array = vfunc(value)
76+
return new_array
77+
78+
def value(self, value: NDArray[DevState]) -> NDArray[np.str_]:
79+
vfunc = np.vectorize(self._convert)
80+
new_array = vfunc(value)
81+
return new_array

src/ophyd_async/tango/core/_signal.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from tango.asyncio import DeviceProxy as AsyncDeviceProxy
2828

2929
from ._tango_transport import TangoSignalBackend, get_python_type
30+
from ._utils import get_device_trl_and_attr
3031

3132
logger = logging.getLogger("ophyd_async")
3233

@@ -35,16 +36,14 @@ def make_backend(
3536
datatype: type[SignalDatatypeT] | None,
3637
read_trl: str = "",
3738
write_trl: str = "",
38-
device_proxy: DeviceProxy | None = None,
3939
) -> TangoSignalBackend:
40-
return TangoSignalBackend(datatype, read_trl, write_trl, device_proxy)
40+
return TangoSignalBackend(datatype, read_trl, write_trl)
4141

4242

4343
def tango_signal_rw(
4444
datatype: type[SignalDatatypeT],
4545
read_trl: str,
4646
write_trl: str = "",
47-
device_proxy: DeviceProxy | None = None,
4847
timeout: float = DEFAULT_TIMEOUT,
4948
name: str = "",
5049
) -> SignalRW[SignalDatatypeT]:
@@ -58,22 +57,19 @@ def tango_signal_rw(
5857
The Attribute/Command to read and monitor
5958
write_trl:
6059
If given, use this Attribute/Command to write to, otherwise use read_trl
61-
device_proxy:
62-
If given, this DeviceProxy will be used
6360
timeout:
6461
The timeout for the read and write operations
6562
name:
6663
The name of the Signal
6764
6865
"""
69-
backend = make_backend(datatype, read_trl, write_trl or read_trl, device_proxy)
66+
backend = make_backend(datatype, read_trl, write_trl or read_trl)
7067
return SignalRW(backend, timeout=timeout, name=name)
7168

7269

7370
def tango_signal_r(
7471
datatype: type[SignalDatatypeT],
7572
read_trl: str,
76-
device_proxy: DeviceProxy | None = None,
7773
timeout: float = DEFAULT_TIMEOUT,
7874
name: str = "",
7975
) -> SignalR[SignalDatatypeT]:
@@ -85,22 +81,19 @@ def tango_signal_r(
8581
Check that the Attribute/Command is of this type
8682
read_trl:
8783
The Attribute/Command to read and monitor
88-
device_proxy:
89-
If given, this DeviceProxy will be used
9084
timeout:
9185
The timeout for the read operation
9286
name:
9387
The name of the Signal
9488
9589
"""
96-
backend = make_backend(datatype, read_trl, read_trl, device_proxy)
90+
backend = make_backend(datatype, read_trl, read_trl)
9791
return SignalR(backend, timeout=timeout, name=name)
9892

9993

10094
def tango_signal_w(
10195
datatype: type[SignalDatatypeT],
10296
write_trl: str,
103-
device_proxy: DeviceProxy | None = None,
10497
timeout: float = DEFAULT_TIMEOUT,
10598
name: str = "",
10699
) -> SignalW[SignalDatatypeT]:
@@ -112,21 +105,18 @@ def tango_signal_w(
112105
Check that the Attribute/Command is of this type
113106
write_trl:
114107
The Attribute/Command to write to
115-
device_proxy:
116-
If given, this DeviceProxy will be used
117108
timeout:
118109
The timeout for the write operation
119110
name:
120111
The name of the Signal
121112
122113
"""
123-
backend = make_backend(datatype, write_trl, write_trl, device_proxy)
114+
backend = make_backend(datatype, write_trl, write_trl)
124115
return SignalW(backend, timeout=timeout, name=name)
125116

126117

127118
def tango_signal_x(
128119
write_trl: str,
129-
device_proxy: DeviceProxy | None = None,
130120
timeout: float = DEFAULT_TIMEOUT,
131121
name: str = "",
132122
) -> SignalX:
@@ -136,15 +126,13 @@ def tango_signal_x(
136126
----------
137127
write_trl:
138128
The Attribute/Command to write its initial value to on execute
139-
device_proxy:
140-
If given, this DeviceProxy will be used
141129
timeout:
142130
The timeout for the command operation
143131
name:
144132
The name of the Signal
145133
146134
"""
147-
backend = make_backend(None, write_trl, write_trl, device_proxy)
135+
backend = make_backend(None, write_trl, write_trl)
148136
return SignalX(backend, timeout=timeout, name=name)
149137

150138

@@ -153,7 +141,7 @@ async def infer_python_type(
153141
) -> object | npt.NDArray | type[DevState] | IntEnum:
154142
"""Infers the python type from the TRL."""
155143
# TODO: work out if this is still needed
156-
device_trl, tr_name = trl.rsplit("/", 1)
144+
device_trl, tr_name = get_device_trl_and_attr(trl)
157145
if proxy is None:
158146
dev_proxy = await AsyncDeviceProxy(device_trl)
159147
else:
@@ -182,7 +170,7 @@ async def infer_python_type(
182170
async def infer_signal_type(
183171
trl, proxy: DeviceProxy | None = None
184172
) -> type[Signal] | None:
185-
device_trl, tr_name = trl.rsplit("/", 1)
173+
device_trl, tr_name = get_device_trl_and_attr(trl)
186174
if proxy is None:
187175
dev_proxy = await AsyncDeviceProxy(device_trl)
188176
else:

0 commit comments

Comments
 (0)