ophyd-async 0.5.2__py3-none-any.whl → 0.7.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ophyd_async/__init__.py +10 -1
- ophyd_async/__main__.py +12 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +15 -7
- ophyd_async/core/_detector.py +133 -87
- ophyd_async/core/_device.py +13 -15
- ophyd_async/core/_device_save_loader.py +30 -19
- ophyd_async/core/_flyer.py +6 -19
- ophyd_async/core/_hdf_dataset.py +8 -9
- ophyd_async/core/_log.py +3 -1
- ophyd_async/core/_mock_signal_backend.py +11 -9
- ophyd_async/core/_mock_signal_utils.py +8 -5
- ophyd_async/core/_protocol.py +7 -7
- ophyd_async/core/_providers.py +11 -11
- ophyd_async/core/_readable.py +30 -22
- ophyd_async/core/_signal.py +52 -51
- ophyd_async/core/_signal_backend.py +20 -7
- ophyd_async/core/_soft_signal_backend.py +62 -32
- ophyd_async/core/_status.py +7 -9
- ophyd_async/core/_table.py +146 -0
- ophyd_async/core/_utils.py +24 -28
- ophyd_async/epics/adaravis/_aravis_controller.py +20 -19
- ophyd_async/epics/adaravis/_aravis_io.py +2 -1
- ophyd_async/epics/adcore/_core_io.py +2 -0
- ophyd_async/epics/adcore/_core_logic.py +4 -5
- ophyd_async/epics/adcore/_hdf_writer.py +19 -8
- ophyd_async/epics/adcore/_single_trigger.py +1 -1
- ophyd_async/epics/adcore/_utils.py +5 -6
- ophyd_async/epics/adkinetix/_kinetix_controller.py +20 -15
- ophyd_async/epics/adpilatus/_pilatus_controller.py +22 -18
- ophyd_async/epics/adsimdetector/_sim.py +7 -6
- ophyd_async/epics/adsimdetector/_sim_controller.py +22 -17
- ophyd_async/epics/advimba/_vimba_controller.py +22 -17
- ophyd_async/epics/demo/_mover.py +4 -5
- ophyd_async/epics/demo/sensor.db +0 -1
- ophyd_async/epics/eiger/_eiger.py +1 -1
- ophyd_async/epics/eiger/_eiger_controller.py +18 -18
- ophyd_async/epics/eiger/_odin_io.py +6 -5
- ophyd_async/epics/motor.py +8 -10
- ophyd_async/epics/pvi/_pvi.py +30 -33
- ophyd_async/epics/signal/_aioca.py +55 -25
- ophyd_async/epics/signal/_common.py +3 -10
- ophyd_async/epics/signal/_epics_transport.py +11 -8
- ophyd_async/epics/signal/_p4p.py +79 -30
- ophyd_async/epics/signal/_signal.py +6 -8
- ophyd_async/fastcs/panda/__init__.py +0 -6
- ophyd_async/fastcs/panda/_control.py +16 -17
- ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
- ophyd_async/fastcs/panda/_table.py +77 -138
- ophyd_async/fastcs/panda/_trigger.py +4 -5
- ophyd_async/fastcs/panda/_utils.py +3 -2
- ophyd_async/fastcs/panda/_writer.py +28 -13
- ophyd_async/plan_stubs/_fly.py +15 -17
- ophyd_async/plan_stubs/_nd_attributes.py +12 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +27 -21
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
- ophyd_async/sim/demo/_sim_motor.py +2 -1
- ophyd_async/tango/__init__.py +45 -0
- ophyd_async/tango/base_devices/__init__.py +4 -0
- ophyd_async/tango/base_devices/_base_device.py +225 -0
- ophyd_async/tango/base_devices/_tango_readable.py +33 -0
- ophyd_async/tango/demo/__init__.py +12 -0
- ophyd_async/tango/demo/_counter.py +37 -0
- ophyd_async/tango/demo/_detector.py +42 -0
- ophyd_async/tango/demo/_mover.py +77 -0
- ophyd_async/tango/demo/_tango/__init__.py +3 -0
- ophyd_async/tango/demo/_tango/_servers.py +108 -0
- ophyd_async/tango/signal/__init__.py +39 -0
- ophyd_async/tango/signal/_signal.py +223 -0
- ophyd_async/tango/signal/_tango_transport.py +764 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/METADATA +50 -45
- ophyd_async-0.7.0a1.dist-info/RECORD +108 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/WHEEL +1 -1
- ophyd_async-0.5.2.dist-info/RECORD +0 -95
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/LICENSE +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import time
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from asyncio import CancelledError
|
|
6
|
+
from collections.abc import Callable, Coroutine
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from bluesky.protocols import Descriptor, Reading
|
|
12
|
+
|
|
13
|
+
from ophyd_async.core import (
|
|
14
|
+
DEFAULT_TIMEOUT,
|
|
15
|
+
AsyncStatus,
|
|
16
|
+
NotConnected,
|
|
17
|
+
ReadingValueCallback,
|
|
18
|
+
SignalBackend,
|
|
19
|
+
T,
|
|
20
|
+
get_dtype,
|
|
21
|
+
get_unique,
|
|
22
|
+
wait_for_connection,
|
|
23
|
+
)
|
|
24
|
+
from tango import (
|
|
25
|
+
AttrDataFormat,
|
|
26
|
+
AttributeInfoEx,
|
|
27
|
+
CmdArgType,
|
|
28
|
+
CommandInfo,
|
|
29
|
+
DevFailed, # type: ignore
|
|
30
|
+
DeviceProxy,
|
|
31
|
+
DevState,
|
|
32
|
+
EventType,
|
|
33
|
+
)
|
|
34
|
+
from tango.asyncio import DeviceProxy as AsyncDeviceProxy
|
|
35
|
+
from tango.asyncio_executor import (
|
|
36
|
+
AsyncioExecutor,
|
|
37
|
+
get_global_executor,
|
|
38
|
+
set_global_executor,
|
|
39
|
+
)
|
|
40
|
+
from tango.utils import is_array, is_binary, is_bool, is_float, is_int, is_str
|
|
41
|
+
|
|
42
|
+
# time constant to wait for timeout
|
|
43
|
+
A_BIT = 1e-5
|
|
44
|
+
|
|
45
|
+
R = TypeVar("R")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ensure_proper_executor(
|
|
49
|
+
func: Callable[..., Coroutine[Any, Any, R]],
|
|
50
|
+
) -> Callable[..., Coroutine[Any, Any, R]]:
|
|
51
|
+
@functools.wraps(func)
|
|
52
|
+
async def wrapper(self: Any, *args: Any, **kwargs: Any) -> R:
|
|
53
|
+
current_executor: AsyncioExecutor = get_global_executor() # type: ignore
|
|
54
|
+
if not current_executor.in_executor_context(): # type: ignore
|
|
55
|
+
set_global_executor(AsyncioExecutor())
|
|
56
|
+
return await func(self, *args, **kwargs)
|
|
57
|
+
|
|
58
|
+
return cast(Callable[..., Coroutine[Any, Any, R]], wrapper)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_python_type(tango_type: CmdArgType) -> tuple[bool, object, str]:
|
|
62
|
+
array = is_array(tango_type)
|
|
63
|
+
if is_int(tango_type, True):
|
|
64
|
+
return array, int, "integer"
|
|
65
|
+
if is_float(tango_type, True):
|
|
66
|
+
return array, float, "number"
|
|
67
|
+
if is_bool(tango_type, True):
|
|
68
|
+
return array, bool, "integer"
|
|
69
|
+
if is_str(tango_type, True):
|
|
70
|
+
return array, str, "string"
|
|
71
|
+
if is_binary(tango_type, True):
|
|
72
|
+
return array, list[str], "string"
|
|
73
|
+
if tango_type == CmdArgType.DevEnum:
|
|
74
|
+
return array, Enum, "string"
|
|
75
|
+
if tango_type == CmdArgType.DevState:
|
|
76
|
+
return array, CmdArgType.DevState, "string"
|
|
77
|
+
if tango_type == CmdArgType.DevUChar:
|
|
78
|
+
return array, int, "integer"
|
|
79
|
+
if tango_type == CmdArgType.DevVoid:
|
|
80
|
+
return array, None, "string"
|
|
81
|
+
raise TypeError("Unknown TangoType")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TangoProxy:
|
|
85
|
+
support_events: bool = True
|
|
86
|
+
_proxy: DeviceProxy
|
|
87
|
+
_name: str
|
|
88
|
+
|
|
89
|
+
def __init__(self, device_proxy: DeviceProxy, name: str):
|
|
90
|
+
self._proxy = device_proxy
|
|
91
|
+
self._name = name
|
|
92
|
+
|
|
93
|
+
async def connect(self) -> None:
|
|
94
|
+
"""perform actions after proxy is connected, e.g. checks if signal
|
|
95
|
+
can be subscribed"""
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
async def get(self) -> object:
|
|
99
|
+
"""Get value from TRL"""
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
async def get_w_value(self) -> object:
|
|
103
|
+
"""Get last written value from TRL"""
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
async def put(
|
|
107
|
+
self, value: object | None, wait: bool = True, timeout: float | None = None
|
|
108
|
+
) -> AsyncStatus | None:
|
|
109
|
+
"""Put value to TRL"""
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
async def get_config(self) -> AttributeInfoEx | CommandInfo:
|
|
113
|
+
"""Get TRL config async"""
|
|
114
|
+
|
|
115
|
+
@abstractmethod
|
|
116
|
+
async def get_reading(self) -> Reading:
|
|
117
|
+
"""Get reading from TRL"""
|
|
118
|
+
|
|
119
|
+
@abstractmethod
|
|
120
|
+
def has_subscription(self) -> bool:
|
|
121
|
+
"""indicates, that this trl already subscribed"""
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def subscribe_callback(self, callback: ReadingValueCallback | None):
|
|
125
|
+
"""subscribe tango CHANGE event to callback"""
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
def unsubscribe_callback(self):
|
|
129
|
+
"""delete CHANGE event subscription"""
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def set_polling(
|
|
133
|
+
self,
|
|
134
|
+
allow_polling: bool = True,
|
|
135
|
+
polling_period: float = 0.1,
|
|
136
|
+
abs_change=None,
|
|
137
|
+
rel_change=None,
|
|
138
|
+
):
|
|
139
|
+
"""Set polling parameters"""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class AttributeProxy(TangoProxy):
|
|
143
|
+
_callback: ReadingValueCallback | None = None
|
|
144
|
+
_eid: int | None = None
|
|
145
|
+
_poll_task: asyncio.Task | None = None
|
|
146
|
+
_abs_change: float | None = None
|
|
147
|
+
_rel_change: float | None = 0.1
|
|
148
|
+
_polling_period: float = 0.1
|
|
149
|
+
_allow_polling: bool = False
|
|
150
|
+
exception: BaseException | None = None
|
|
151
|
+
_last_reading: Reading = Reading(value=None, timestamp=0, alarm_severity=0)
|
|
152
|
+
|
|
153
|
+
async def connect(self) -> None:
|
|
154
|
+
try:
|
|
155
|
+
# I have to typehint proxy as tango.DeviceProxy because
|
|
156
|
+
# tango.asyncio.DeviceProxy cannot be used as a typehint.
|
|
157
|
+
# This means pyright will not be able to see that
|
|
158
|
+
# subscribe_event is awaitable.
|
|
159
|
+
eid = await self._proxy.subscribe_event( # type: ignore
|
|
160
|
+
self._name, EventType.CHANGE_EVENT, self._event_processor
|
|
161
|
+
)
|
|
162
|
+
await self._proxy.unsubscribe_event(eid)
|
|
163
|
+
self.support_events = True
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
@ensure_proper_executor
|
|
168
|
+
async def get(self) -> Coroutine[Any, Any, object]:
|
|
169
|
+
attr = await self._proxy.read_attribute(self._name)
|
|
170
|
+
return attr.value
|
|
171
|
+
|
|
172
|
+
@ensure_proper_executor
|
|
173
|
+
async def get_w_value(self) -> object:
|
|
174
|
+
attr = await self._proxy.read_attribute(self._name)
|
|
175
|
+
return attr.w_value
|
|
176
|
+
|
|
177
|
+
@ensure_proper_executor
|
|
178
|
+
async def put(
|
|
179
|
+
self, value: object | None, wait: bool = True, timeout: float | None = None
|
|
180
|
+
) -> AsyncStatus | None:
|
|
181
|
+
if wait:
|
|
182
|
+
try:
|
|
183
|
+
|
|
184
|
+
async def _write():
|
|
185
|
+
return await self._proxy.write_attribute(self._name, value)
|
|
186
|
+
|
|
187
|
+
task = asyncio.create_task(_write())
|
|
188
|
+
await asyncio.wait_for(task, timeout)
|
|
189
|
+
except asyncio.TimeoutError as te:
|
|
190
|
+
raise TimeoutError(f"{self._name} attr put failed: Timeout") from te
|
|
191
|
+
except DevFailed as de:
|
|
192
|
+
raise RuntimeError(
|
|
193
|
+
f"{self._name} device" f" failure: {de.args[0].desc}"
|
|
194
|
+
) from de
|
|
195
|
+
|
|
196
|
+
else:
|
|
197
|
+
rid = await self._proxy.write_attribute_asynch(self._name, value)
|
|
198
|
+
|
|
199
|
+
async def wait_for_reply(rd: int, to: float | None):
|
|
200
|
+
start_time = time.time()
|
|
201
|
+
while True:
|
|
202
|
+
try:
|
|
203
|
+
# I have to typehint proxy as tango.DeviceProxy because
|
|
204
|
+
# tango.asyncio.DeviceProxy cannot be used as a typehint.
|
|
205
|
+
# This means pyright will not be able to see that
|
|
206
|
+
# write_attribute_reply is awaitable.
|
|
207
|
+
await self._proxy.write_attribute_reply(rd) # type: ignore
|
|
208
|
+
break
|
|
209
|
+
except DevFailed as exc:
|
|
210
|
+
if exc.args[0].reason == "API_AsynReplyNotArrived":
|
|
211
|
+
await asyncio.sleep(A_BIT)
|
|
212
|
+
if to and (time.time() - start_time > to):
|
|
213
|
+
raise TimeoutError(
|
|
214
|
+
f"{self._name} attr put failed:" f" Timeout"
|
|
215
|
+
) from exc
|
|
216
|
+
else:
|
|
217
|
+
raise RuntimeError(
|
|
218
|
+
f"{self._name} device failure:" f" {exc.args[0].desc}"
|
|
219
|
+
) from exc
|
|
220
|
+
|
|
221
|
+
return AsyncStatus(wait_for_reply(rid, timeout))
|
|
222
|
+
|
|
223
|
+
@ensure_proper_executor
|
|
224
|
+
async def get_config(self) -> AttributeInfoEx:
|
|
225
|
+
return await self._proxy.get_attribute_config(self._name)
|
|
226
|
+
|
|
227
|
+
@ensure_proper_executor
|
|
228
|
+
async def get_reading(self) -> Reading:
|
|
229
|
+
attr = await self._proxy.read_attribute(self._name)
|
|
230
|
+
reading = Reading(
|
|
231
|
+
value=attr.value, timestamp=attr.time.totime(), alarm_severity=attr.quality
|
|
232
|
+
)
|
|
233
|
+
self._last_reading = reading
|
|
234
|
+
return reading
|
|
235
|
+
|
|
236
|
+
def has_subscription(self) -> bool:
|
|
237
|
+
return bool(self._callback)
|
|
238
|
+
|
|
239
|
+
def subscribe_callback(self, callback: ReadingValueCallback | None):
|
|
240
|
+
# If the attribute supports events, then we can subscribe to them
|
|
241
|
+
# If the callback is not a callable, then we raise an error
|
|
242
|
+
if callback is not None and not callable(callback):
|
|
243
|
+
raise RuntimeError("Callback must be a callable")
|
|
244
|
+
|
|
245
|
+
self._callback = callback
|
|
246
|
+
if self.support_events:
|
|
247
|
+
"""add user callback to CHANGE event subscription"""
|
|
248
|
+
if not self._eid:
|
|
249
|
+
self._eid = self._proxy.subscribe_event(
|
|
250
|
+
self._name,
|
|
251
|
+
EventType.CHANGE_EVENT,
|
|
252
|
+
self._event_processor,
|
|
253
|
+
green_mode=False,
|
|
254
|
+
)
|
|
255
|
+
elif self._allow_polling:
|
|
256
|
+
"""start polling if no events supported"""
|
|
257
|
+
if self._callback is not None:
|
|
258
|
+
|
|
259
|
+
async def _poll():
|
|
260
|
+
while True:
|
|
261
|
+
try:
|
|
262
|
+
await self.poll()
|
|
263
|
+
except RuntimeError as exc:
|
|
264
|
+
self.exception = exc
|
|
265
|
+
await asyncio.sleep(1)
|
|
266
|
+
|
|
267
|
+
self._poll_task = asyncio.create_task(_poll())
|
|
268
|
+
else:
|
|
269
|
+
self.unsubscribe_callback()
|
|
270
|
+
raise RuntimeError(
|
|
271
|
+
f"Cannot set event for {self._name}. "
|
|
272
|
+
"Cannot set a callback on an attribute that does not support events and"
|
|
273
|
+
" for which polling is disabled."
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
def unsubscribe_callback(self):
|
|
277
|
+
if self._eid:
|
|
278
|
+
self._proxy.unsubscribe_event(self._eid, green_mode=False)
|
|
279
|
+
self._eid = None
|
|
280
|
+
if self._poll_task:
|
|
281
|
+
self._poll_task.cancel()
|
|
282
|
+
self._poll_task = None
|
|
283
|
+
if self._callback is not None:
|
|
284
|
+
# Call the callback with the last reading
|
|
285
|
+
try:
|
|
286
|
+
self._callback(self._last_reading, self._last_reading["value"])
|
|
287
|
+
except TypeError:
|
|
288
|
+
pass
|
|
289
|
+
self._callback = None
|
|
290
|
+
|
|
291
|
+
def _event_processor(self, event):
|
|
292
|
+
if not event.err:
|
|
293
|
+
value = event.attr_value.value
|
|
294
|
+
reading = Reading(
|
|
295
|
+
value=value,
|
|
296
|
+
timestamp=event.get_date().totime(),
|
|
297
|
+
alarm_severity=event.attr_value.quality,
|
|
298
|
+
)
|
|
299
|
+
if self._callback is not None:
|
|
300
|
+
self._callback(reading, value)
|
|
301
|
+
|
|
302
|
+
async def poll(self):
|
|
303
|
+
"""
|
|
304
|
+
Poll the attribute and call the callback if the value has changed by more
|
|
305
|
+
than the absolute or relative change. This function is used when an attribute
|
|
306
|
+
that does not support events is cached or a callback is passed to it.
|
|
307
|
+
"""
|
|
308
|
+
try:
|
|
309
|
+
last_reading = await self.get_reading()
|
|
310
|
+
flag = 0
|
|
311
|
+
# Initial reading
|
|
312
|
+
if self._callback is not None:
|
|
313
|
+
self._callback(last_reading, last_reading["value"])
|
|
314
|
+
except Exception as e:
|
|
315
|
+
raise RuntimeError(f"Could not poll the attribute: {e}") from e
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
# If the value is a number, we can check for changes
|
|
319
|
+
if isinstance(last_reading["value"], int | float):
|
|
320
|
+
while True:
|
|
321
|
+
await asyncio.sleep(self._polling_period)
|
|
322
|
+
reading = await self.get_reading()
|
|
323
|
+
if reading is None or reading["value"] is None:
|
|
324
|
+
continue
|
|
325
|
+
diff = abs(reading["value"] - last_reading["value"])
|
|
326
|
+
if self._abs_change is not None and diff >= abs(self._abs_change):
|
|
327
|
+
if self._callback is not None:
|
|
328
|
+
self._callback(reading, reading["value"])
|
|
329
|
+
flag = 0
|
|
330
|
+
|
|
331
|
+
elif (
|
|
332
|
+
self._rel_change is not None
|
|
333
|
+
and diff >= self._rel_change * abs(last_reading["value"])
|
|
334
|
+
):
|
|
335
|
+
if self._callback is not None:
|
|
336
|
+
self._callback(reading, reading["value"])
|
|
337
|
+
flag = 0
|
|
338
|
+
|
|
339
|
+
else:
|
|
340
|
+
flag = (flag + 1) % 4
|
|
341
|
+
if flag == 0 and self._callback is not None:
|
|
342
|
+
self._callback(reading, reading["value"])
|
|
343
|
+
|
|
344
|
+
last_reading = reading.copy()
|
|
345
|
+
if self._callback is None:
|
|
346
|
+
break
|
|
347
|
+
# If the value is not a number, we can only poll
|
|
348
|
+
else:
|
|
349
|
+
while True:
|
|
350
|
+
await asyncio.sleep(self._polling_period)
|
|
351
|
+
flag = (flag + 1) % 4
|
|
352
|
+
if flag == 0:
|
|
353
|
+
reading = await self.get_reading()
|
|
354
|
+
if reading is None or reading["value"] is None:
|
|
355
|
+
continue
|
|
356
|
+
if isinstance(reading["value"], np.ndarray):
|
|
357
|
+
if not np.array_equal(
|
|
358
|
+
reading["value"], last_reading["value"]
|
|
359
|
+
):
|
|
360
|
+
if self._callback is not None:
|
|
361
|
+
self._callback(reading, reading["value"])
|
|
362
|
+
else:
|
|
363
|
+
break
|
|
364
|
+
else:
|
|
365
|
+
if reading["value"] != last_reading["value"]:
|
|
366
|
+
if self._callback is not None:
|
|
367
|
+
self._callback(reading, reading["value"])
|
|
368
|
+
else:
|
|
369
|
+
break
|
|
370
|
+
last_reading = reading.copy()
|
|
371
|
+
except Exception as e:
|
|
372
|
+
raise RuntimeError(f"Could not poll the attribute: {e}") from e
|
|
373
|
+
|
|
374
|
+
def set_polling(
|
|
375
|
+
self,
|
|
376
|
+
allow_polling: bool = False,
|
|
377
|
+
polling_period: float = 0.5,
|
|
378
|
+
abs_change: float | None = None,
|
|
379
|
+
rel_change: float | None = 0.1,
|
|
380
|
+
):
|
|
381
|
+
"""
|
|
382
|
+
Set the polling parameters.
|
|
383
|
+
"""
|
|
384
|
+
self._allow_polling = allow_polling
|
|
385
|
+
self._polling_period = polling_period
|
|
386
|
+
self._abs_change = abs_change
|
|
387
|
+
self._rel_change = rel_change
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class CommandProxy(TangoProxy):
|
|
391
|
+
_last_reading: Reading = Reading(value=None, timestamp=0, alarm_severity=0)
|
|
392
|
+
|
|
393
|
+
def subscribe_callback(self, callback: ReadingValueCallback | None) -> None:
|
|
394
|
+
raise NotImplementedError("Cannot subscribe to commands")
|
|
395
|
+
|
|
396
|
+
def unsubscribe_callback(self) -> None:
|
|
397
|
+
raise NotImplementedError("Cannot unsubscribe from commands")
|
|
398
|
+
|
|
399
|
+
async def get(self) -> object:
|
|
400
|
+
return self._last_reading["value"]
|
|
401
|
+
|
|
402
|
+
async def get_w_value(self) -> object:
|
|
403
|
+
return self._last_reading["value"]
|
|
404
|
+
|
|
405
|
+
async def connect(self) -> None:
|
|
406
|
+
pass
|
|
407
|
+
|
|
408
|
+
@ensure_proper_executor
|
|
409
|
+
async def put(
|
|
410
|
+
self, value: object | None, wait: bool = True, timeout: float | None = None
|
|
411
|
+
) -> AsyncStatus | None:
|
|
412
|
+
if wait:
|
|
413
|
+
try:
|
|
414
|
+
|
|
415
|
+
async def _put():
|
|
416
|
+
return await self._proxy.command_inout(self._name, value)
|
|
417
|
+
|
|
418
|
+
task = asyncio.create_task(_put())
|
|
419
|
+
val = await asyncio.wait_for(task, timeout)
|
|
420
|
+
self._last_reading = Reading(
|
|
421
|
+
value=val, timestamp=time.time(), alarm_severity=0
|
|
422
|
+
)
|
|
423
|
+
except asyncio.TimeoutError as te:
|
|
424
|
+
raise TimeoutError(f"{self._name} command failed: Timeout") from te
|
|
425
|
+
except DevFailed as de:
|
|
426
|
+
raise RuntimeError(
|
|
427
|
+
f"{self._name} device" f" failure: {de.args[0].desc}"
|
|
428
|
+
) from de
|
|
429
|
+
|
|
430
|
+
else:
|
|
431
|
+
rid = self._proxy.command_inout_asynch(self._name, value)
|
|
432
|
+
|
|
433
|
+
async def wait_for_reply(rd: int, to: float | None):
|
|
434
|
+
start_time = time.time()
|
|
435
|
+
while True:
|
|
436
|
+
try:
|
|
437
|
+
reply_value = self._proxy.command_inout_reply(rd)
|
|
438
|
+
self._last_reading = Reading(
|
|
439
|
+
value=reply_value, timestamp=time.time(), alarm_severity=0
|
|
440
|
+
)
|
|
441
|
+
break
|
|
442
|
+
except DevFailed as de_exc:
|
|
443
|
+
if de_exc.args[0].reason == "API_AsynReplyNotArrived":
|
|
444
|
+
await asyncio.sleep(A_BIT)
|
|
445
|
+
if to and time.time() - start_time > to:
|
|
446
|
+
raise TimeoutError(
|
|
447
|
+
"Timeout while waiting for command reply"
|
|
448
|
+
) from de_exc
|
|
449
|
+
else:
|
|
450
|
+
raise RuntimeError(
|
|
451
|
+
f"{self._name} device failure:"
|
|
452
|
+
f" {de_exc.args[0].desc}"
|
|
453
|
+
) from de_exc
|
|
454
|
+
|
|
455
|
+
return AsyncStatus(wait_for_reply(rid, timeout))
|
|
456
|
+
|
|
457
|
+
@ensure_proper_executor
|
|
458
|
+
async def get_config(self) -> CommandInfo:
|
|
459
|
+
return await self._proxy.get_command_config(self._name)
|
|
460
|
+
|
|
461
|
+
async def get_reading(self) -> Reading:
|
|
462
|
+
reading = Reading(
|
|
463
|
+
value=self._last_reading["value"],
|
|
464
|
+
timestamp=self._last_reading["timestamp"],
|
|
465
|
+
alarm_severity=self._last_reading.get("alarm_severity", 0),
|
|
466
|
+
)
|
|
467
|
+
return reading
|
|
468
|
+
|
|
469
|
+
def set_polling(
|
|
470
|
+
self,
|
|
471
|
+
allow_polling: bool = False,
|
|
472
|
+
polling_period: float = 0.5,
|
|
473
|
+
abs_change: float | None = None,
|
|
474
|
+
rel_change: float | None = 0.1,
|
|
475
|
+
):
|
|
476
|
+
pass
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def get_dtype_extended(datatype) -> object | None:
|
|
480
|
+
# DevState tango type does not have numpy equivalents
|
|
481
|
+
dtype = get_dtype(datatype)
|
|
482
|
+
if dtype == np.object_:
|
|
483
|
+
if datatype.__args__[1].__args__[0] == DevState:
|
|
484
|
+
dtype = CmdArgType.DevState
|
|
485
|
+
return dtype
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def get_trl_descriptor(
|
|
489
|
+
datatype: type | None,
|
|
490
|
+
tango_resource: str,
|
|
491
|
+
tr_configs: dict[str, AttributeInfoEx | CommandInfo],
|
|
492
|
+
) -> Descriptor:
|
|
493
|
+
tr_dtype = {}
|
|
494
|
+
for tr_name, config in tr_configs.items():
|
|
495
|
+
if isinstance(config, AttributeInfoEx):
|
|
496
|
+
_, dtype, descr = get_python_type(config.data_type)
|
|
497
|
+
tr_dtype[tr_name] = config.data_format, dtype, descr
|
|
498
|
+
elif isinstance(config, CommandInfo):
|
|
499
|
+
if (
|
|
500
|
+
config.in_type != CmdArgType.DevVoid
|
|
501
|
+
and config.out_type != CmdArgType.DevVoid
|
|
502
|
+
and config.in_type != config.out_type
|
|
503
|
+
):
|
|
504
|
+
raise RuntimeError(
|
|
505
|
+
"Commands with different in and out dtypes are not supported"
|
|
506
|
+
)
|
|
507
|
+
array, dtype, descr = get_python_type(
|
|
508
|
+
config.in_type
|
|
509
|
+
if config.in_type != CmdArgType.DevVoid
|
|
510
|
+
else config.out_type
|
|
511
|
+
)
|
|
512
|
+
tr_dtype[tr_name] = (
|
|
513
|
+
AttrDataFormat.SPECTRUM if array else AttrDataFormat.SCALAR,
|
|
514
|
+
dtype,
|
|
515
|
+
descr,
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
raise RuntimeError(f"Unknown config type: {type(config)}")
|
|
519
|
+
tr_format, tr_dtype, tr_dtype_desc = get_unique(tr_dtype, "typeids")
|
|
520
|
+
|
|
521
|
+
# tango commands are limited in functionality:
|
|
522
|
+
# they do not have info about shape and Enum labels
|
|
523
|
+
trl_config = list(tr_configs.values())[0]
|
|
524
|
+
max_x: int = (
|
|
525
|
+
trl_config.max_dim_x
|
|
526
|
+
if hasattr(trl_config, "max_dim_x")
|
|
527
|
+
else np.iinfo(np.int32).max
|
|
528
|
+
)
|
|
529
|
+
max_y: int = (
|
|
530
|
+
trl_config.max_dim_y
|
|
531
|
+
if hasattr(trl_config, "max_dim_y")
|
|
532
|
+
else np.iinfo(np.int32).max
|
|
533
|
+
)
|
|
534
|
+
# is_attr = hasattr(trl_config, "enum_labels")
|
|
535
|
+
# trl_choices = list(trl_config.enum_labels) if is_attr else []
|
|
536
|
+
|
|
537
|
+
if tr_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]:
|
|
538
|
+
# This is an array
|
|
539
|
+
if datatype:
|
|
540
|
+
# Check we wanted an array of this type
|
|
541
|
+
dtype = get_dtype_extended(datatype)
|
|
542
|
+
if not dtype:
|
|
543
|
+
raise TypeError(
|
|
544
|
+
f"{tango_resource} has type [{tr_dtype}] not {datatype.__name__}"
|
|
545
|
+
)
|
|
546
|
+
if dtype != tr_dtype:
|
|
547
|
+
raise TypeError(f"{tango_resource} has type [{tr_dtype}] not [{dtype}]")
|
|
548
|
+
|
|
549
|
+
if tr_format == AttrDataFormat.SPECTRUM:
|
|
550
|
+
return Descriptor(source=tango_resource, dtype="array", shape=[max_x])
|
|
551
|
+
elif tr_format == AttrDataFormat.IMAGE:
|
|
552
|
+
return Descriptor(
|
|
553
|
+
source=tango_resource, dtype="array", shape=[max_y, max_x]
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
else:
|
|
557
|
+
if tr_dtype in (Enum, CmdArgType.DevState):
|
|
558
|
+
# if tr_dtype == CmdArgType.DevState:
|
|
559
|
+
# trl_choices = list(DevState.names.keys())
|
|
560
|
+
|
|
561
|
+
if datatype:
|
|
562
|
+
if not issubclass(datatype, Enum | DevState):
|
|
563
|
+
raise TypeError(
|
|
564
|
+
f"{tango_resource} has type Enum not {datatype.__name__}"
|
|
565
|
+
)
|
|
566
|
+
# if tr_dtype == Enum and is_attr:
|
|
567
|
+
# if isinstance(datatype, DevState):
|
|
568
|
+
# choices = tuple(v.name for v in datatype)
|
|
569
|
+
# if set(choices) != set(trl_choices):
|
|
570
|
+
# raise TypeError(
|
|
571
|
+
# f"{tango_resource} has choices {trl_choices} "
|
|
572
|
+
# f"not {choices}"
|
|
573
|
+
# )
|
|
574
|
+
return Descriptor(source=tango_resource, dtype="string", shape=[])
|
|
575
|
+
else:
|
|
576
|
+
if datatype and not issubclass(tr_dtype, datatype):
|
|
577
|
+
raise TypeError(
|
|
578
|
+
f"{tango_resource} has type {tr_dtype.__name__} "
|
|
579
|
+
f"not {datatype.__name__}"
|
|
580
|
+
)
|
|
581
|
+
return Descriptor(source=tango_resource, dtype=tr_dtype_desc, shape=[])
|
|
582
|
+
|
|
583
|
+
raise RuntimeError(f"Error getting descriptor for {tango_resource}")
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
async def get_tango_trl(
|
|
587
|
+
full_trl: str, device_proxy: DeviceProxy | TangoProxy | None
|
|
588
|
+
) -> TangoProxy:
|
|
589
|
+
if isinstance(device_proxy, TangoProxy):
|
|
590
|
+
return device_proxy
|
|
591
|
+
device_trl, trl_name = full_trl.rsplit("/", 1)
|
|
592
|
+
trl_name = trl_name.lower()
|
|
593
|
+
if device_proxy is None:
|
|
594
|
+
device_proxy = await AsyncDeviceProxy(device_trl)
|
|
595
|
+
|
|
596
|
+
# all attributes can be always accessible with low register
|
|
597
|
+
if isinstance(device_proxy, DeviceProxy):
|
|
598
|
+
all_attrs = [
|
|
599
|
+
attr_name.lower() for attr_name in device_proxy.get_attribute_list()
|
|
600
|
+
]
|
|
601
|
+
else:
|
|
602
|
+
raise TypeError(
|
|
603
|
+
f"device_proxy must be an instance of DeviceProxy for {full_trl}"
|
|
604
|
+
)
|
|
605
|
+
if trl_name in all_attrs:
|
|
606
|
+
return AttributeProxy(device_proxy, trl_name)
|
|
607
|
+
|
|
608
|
+
# all commands can be always accessible with low register
|
|
609
|
+
all_cmds = [cmd_name.lower() for cmd_name in device_proxy.get_command_list()]
|
|
610
|
+
if trl_name in all_cmds:
|
|
611
|
+
return CommandProxy(device_proxy, trl_name)
|
|
612
|
+
|
|
613
|
+
# If version is below tango 9, then pipes are not supported
|
|
614
|
+
if device_proxy.info().server_version >= 9:
|
|
615
|
+
# all pipes can be always accessible with low register
|
|
616
|
+
all_pipes = [pipe_name.lower() for pipe_name in device_proxy.get_pipe_list()]
|
|
617
|
+
if trl_name in all_pipes:
|
|
618
|
+
raise NotImplementedError("Pipes are not supported")
|
|
619
|
+
|
|
620
|
+
raise RuntimeError(f"{trl_name} cannot be found in {device_proxy.name()}")
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
class TangoSignalBackend(SignalBackend[T]):
|
|
624
|
+
def __init__(
|
|
625
|
+
self,
|
|
626
|
+
datatype: type[T] | None,
|
|
627
|
+
read_trl: str = "",
|
|
628
|
+
write_trl: str = "",
|
|
629
|
+
device_proxy: DeviceProxy | None = None,
|
|
630
|
+
):
|
|
631
|
+
self.device_proxy = device_proxy
|
|
632
|
+
self.datatype = datatype
|
|
633
|
+
self.read_trl = read_trl
|
|
634
|
+
self.write_trl = write_trl
|
|
635
|
+
self.proxies: dict[str, TangoProxy | DeviceProxy | None] = {
|
|
636
|
+
read_trl: self.device_proxy,
|
|
637
|
+
write_trl: self.device_proxy,
|
|
638
|
+
}
|
|
639
|
+
self.trl_configs: dict[str, AttributeInfoEx] = {}
|
|
640
|
+
self.descriptor: Descriptor = {} # type: ignore
|
|
641
|
+
self._polling: tuple[bool, float, float | None, float | None] = (
|
|
642
|
+
False,
|
|
643
|
+
0.1,
|
|
644
|
+
None,
|
|
645
|
+
0.1,
|
|
646
|
+
)
|
|
647
|
+
self.support_events: bool = True
|
|
648
|
+
self.status: AsyncStatus | None = None
|
|
649
|
+
|
|
650
|
+
@classmethod
|
|
651
|
+
def datatype_allowed(cls, dtype: Any) -> bool:
|
|
652
|
+
return dtype in (int, float, str, bool, np.ndarray, Enum, DevState)
|
|
653
|
+
|
|
654
|
+
def set_trl(self, read_trl: str = "", write_trl: str = ""):
|
|
655
|
+
self.read_trl = read_trl
|
|
656
|
+
self.write_trl = write_trl if write_trl else read_trl
|
|
657
|
+
self.proxies = {
|
|
658
|
+
read_trl: self.device_proxy,
|
|
659
|
+
write_trl: self.device_proxy,
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
def source(self, name: str) -> str:
|
|
663
|
+
return self.read_trl
|
|
664
|
+
|
|
665
|
+
async def _connect_and_store_config(self, trl: str) -> None:
|
|
666
|
+
if not trl:
|
|
667
|
+
raise RuntimeError(f"trl not set for {self}")
|
|
668
|
+
try:
|
|
669
|
+
self.proxies[trl] = await get_tango_trl(trl, self.proxies[trl])
|
|
670
|
+
if self.proxies[trl] is None:
|
|
671
|
+
raise NotConnected(f"Not connected to {trl}")
|
|
672
|
+
# Pyright does not believe that self.proxies[trl] is not None despite
|
|
673
|
+
# the check above
|
|
674
|
+
await self.proxies[trl].connect() # type: ignore
|
|
675
|
+
self.trl_configs[trl] = await self.proxies[trl].get_config() # type: ignore
|
|
676
|
+
self.proxies[trl].support_events = self.support_events # type: ignore
|
|
677
|
+
except CancelledError as ce:
|
|
678
|
+
raise NotConnected(f"Could not connect to {trl}") from ce
|
|
679
|
+
|
|
680
|
+
async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
|
|
681
|
+
if not self.read_trl:
|
|
682
|
+
raise RuntimeError(f"trl not set for {self}")
|
|
683
|
+
if self.read_trl != self.write_trl:
|
|
684
|
+
# Different, need to connect both
|
|
685
|
+
await wait_for_connection(
|
|
686
|
+
read_trl=self._connect_and_store_config(self.read_trl),
|
|
687
|
+
write_trl=self._connect_and_store_config(self.write_trl),
|
|
688
|
+
)
|
|
689
|
+
else:
|
|
690
|
+
# The same, so only need to connect one
|
|
691
|
+
await self._connect_and_store_config(self.read_trl)
|
|
692
|
+
self.proxies[self.read_trl].set_polling(*self._polling) # type: ignore
|
|
693
|
+
self.descriptor = get_trl_descriptor(
|
|
694
|
+
self.datatype, self.read_trl, self.trl_configs
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
async def put(self, value: T | None, wait=True, timeout=None) -> None:
|
|
698
|
+
if self.proxies[self.write_trl] is None:
|
|
699
|
+
raise NotConnected(f"Not connected to {self.write_trl}")
|
|
700
|
+
self.status = None
|
|
701
|
+
put_status = await self.proxies[self.write_trl].put(value, wait, timeout) # type: ignore
|
|
702
|
+
self.status = put_status
|
|
703
|
+
|
|
704
|
+
async def get_datakey(self, source: str) -> Descriptor:
|
|
705
|
+
return self.descriptor
|
|
706
|
+
|
|
707
|
+
async def get_reading(self) -> Reading:
|
|
708
|
+
if self.proxies[self.read_trl] is None:
|
|
709
|
+
raise NotConnected(f"Not connected to {self.read_trl}")
|
|
710
|
+
return await self.proxies[self.read_trl].get_reading() # type: ignore
|
|
711
|
+
|
|
712
|
+
async def get_value(self) -> T:
|
|
713
|
+
if self.proxies[self.read_trl] is None:
|
|
714
|
+
raise NotConnected(f"Not connected to {self.read_trl}")
|
|
715
|
+
proxy = self.proxies[self.read_trl]
|
|
716
|
+
if proxy is None:
|
|
717
|
+
raise NotConnected(f"Not connected to {self.read_trl}")
|
|
718
|
+
return cast(T, await proxy.get())
|
|
719
|
+
|
|
720
|
+
async def get_setpoint(self) -> T:
|
|
721
|
+
if self.proxies[self.write_trl] is None:
|
|
722
|
+
raise NotConnected(f"Not connected to {self.write_trl}")
|
|
723
|
+
proxy = self.proxies[self.write_trl]
|
|
724
|
+
if proxy is None:
|
|
725
|
+
raise NotConnected(f"Not connected to {self.write_trl}")
|
|
726
|
+
return cast(T, await proxy.get_w_value())
|
|
727
|
+
|
|
728
|
+
def set_callback(self, callback: ReadingValueCallback | None) -> None:
|
|
729
|
+
if self.proxies[self.read_trl] is None:
|
|
730
|
+
raise NotConnected(f"Not connected to {self.read_trl}")
|
|
731
|
+
if self.support_events is False and self._polling[0] is False:
|
|
732
|
+
raise RuntimeError(
|
|
733
|
+
f"Cannot set event for {self.read_trl}. "
|
|
734
|
+
"Cannot set a callback on an attribute that does not support events and"
|
|
735
|
+
" for which polling is disabled."
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
if callback:
|
|
739
|
+
try:
|
|
740
|
+
assert not self.proxies[self.read_trl].has_subscription() # type: ignore
|
|
741
|
+
self.proxies[self.read_trl].subscribe_callback(callback) # type: ignore
|
|
742
|
+
except AssertionError as ae:
|
|
743
|
+
raise RuntimeError(
|
|
744
|
+
"Cannot set a callback when one" " is already set"
|
|
745
|
+
) from ae
|
|
746
|
+
except RuntimeError as exc:
|
|
747
|
+
raise RuntimeError(
|
|
748
|
+
f"Cannot set callback" f" for {self.read_trl}. {exc}"
|
|
749
|
+
) from exc
|
|
750
|
+
|
|
751
|
+
else:
|
|
752
|
+
self.proxies[self.read_trl].unsubscribe_callback() # type: ignore
|
|
753
|
+
|
|
754
|
+
def set_polling(
|
|
755
|
+
self,
|
|
756
|
+
allow_polling: bool = True,
|
|
757
|
+
polling_period: float = 0.1,
|
|
758
|
+
abs_change: float | None = None,
|
|
759
|
+
rel_change: float | None = 0.1,
|
|
760
|
+
):
|
|
761
|
+
self._polling = (allow_polling, polling_period, abs_change, rel_change)
|
|
762
|
+
|
|
763
|
+
def allow_events(self, allow: bool = True):
|
|
764
|
+
self.support_events = allow
|