ophyd-async 0.13.4__py3-none-any.whl → 0.13.6__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/_version.py +2 -2
- ophyd_async/core/__init__.py +24 -2
- ophyd_async/core/_derived_signal_backend.py +2 -1
- ophyd_async/core/_detector.py +2 -2
- ophyd_async/core/_device.py +9 -9
- ophyd_async/core/_enums.py +5 -0
- ophyd_async/core/_mock_signal_backend.py +7 -3
- ophyd_async/core/_signal.py +34 -38
- ophyd_async/core/_signal_backend.py +3 -1
- ophyd_async/core/_status.py +2 -2
- ophyd_async/core/_utils.py +11 -11
- ophyd_async/epics/adcore/_utils.py +4 -4
- ophyd_async/epics/core/_aioca.py +2 -2
- ophyd_async/epics/core/_p4p.py +2 -2
- ophyd_async/epics/motor.py +28 -7
- ophyd_async/epics/pmac/_pmac_trajectory_generation.py +3 -0
- ophyd_async/fastcs/panda/_block.py +10 -9
- ophyd_async/sim/_motor.py +4 -2
- ophyd_async/sim/_stage.py +14 -4
- ophyd_async/tango/core/__init__.py +17 -3
- ophyd_async/tango/core/_signal.py +18 -22
- ophyd_async/tango/core/_tango_transport.py +407 -239
- ophyd_async/tango/core/_utils.py +9 -0
- ophyd_async/tango/demo/_mover.py +1 -2
- ophyd_async/tango/testing/__init__.py +2 -1
- ophyd_async/tango/testing/_one_of_everything.py +13 -5
- ophyd_async/tango/testing/_test_config.py +11 -0
- ophyd_async/testing/_assert.py +2 -2
- ophyd_async/testing/_mock_signal_utils.py +5 -2
- {ophyd_async-0.13.4.dist-info → ophyd_async-0.13.6.dist-info}/METADATA +2 -36
- {ophyd_async-0.13.4.dist-info → ophyd_async-0.13.6.dist-info}/RECORD +34 -33
- {ophyd_async-0.13.4.dist-info → ophyd_async-0.13.6.dist-info}/WHEEL +0 -0
- {ophyd_async-0.13.4.dist-info → ophyd_async-0.13.6.dist-info}/licenses/LICENSE +0 -0
- {ophyd_async-0.13.4.dist-info → ophyd_async-0.13.6.dist-info}/top_level.txt +0 -0
|
@@ -3,15 +3,24 @@ import functools
|
|
|
3
3
|
import logging
|
|
4
4
|
import time
|
|
5
5
|
from abc import abstractmethod
|
|
6
|
-
from collections.abc import Callable, Coroutine
|
|
7
|
-
from
|
|
8
|
-
|
|
6
|
+
from collections.abc import Callable, Coroutine, Sequence
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
ParamSpec,
|
|
10
|
+
TypeVar,
|
|
11
|
+
cast,
|
|
12
|
+
get_args,
|
|
13
|
+
get_origin,
|
|
14
|
+
)
|
|
9
15
|
|
|
10
16
|
import numpy as np
|
|
17
|
+
import numpy.typing as npt
|
|
11
18
|
from bluesky.protocols import Reading
|
|
12
|
-
from event_model import DataKey
|
|
19
|
+
from event_model import DataKey, Limits, LimitsRange
|
|
20
|
+
from event_model.documents.event_descriptor import RdsRange
|
|
13
21
|
from tango import (
|
|
14
22
|
AttrDataFormat,
|
|
23
|
+
AttributeInfo,
|
|
15
24
|
AttributeInfoEx,
|
|
16
25
|
CmdArgType,
|
|
17
26
|
CommandInfo,
|
|
@@ -19,6 +28,7 @@ from tango import (
|
|
|
19
28
|
DeviceProxy,
|
|
20
29
|
DevState,
|
|
21
30
|
EventType,
|
|
31
|
+
GreenMode,
|
|
22
32
|
)
|
|
23
33
|
from tango.asyncio import DeviceProxy as AsyncDeviceProxy
|
|
24
34
|
from tango.asyncio_executor import (
|
|
@@ -26,19 +36,23 @@ from tango.asyncio_executor import (
|
|
|
26
36
|
get_global_executor,
|
|
27
37
|
set_global_executor,
|
|
28
38
|
)
|
|
29
|
-
from tango.utils import
|
|
39
|
+
from tango.utils import is_binary, is_bool, is_float, is_int, is_str
|
|
30
40
|
|
|
31
41
|
from ophyd_async.core import (
|
|
42
|
+
Array1D,
|
|
32
43
|
AsyncStatus,
|
|
33
44
|
Callback,
|
|
34
|
-
|
|
45
|
+
NotConnectedError,
|
|
35
46
|
SignalBackend,
|
|
36
47
|
SignalDatatypeT,
|
|
48
|
+
SignalMetadata,
|
|
37
49
|
StrictEnum,
|
|
50
|
+
Table,
|
|
38
51
|
get_dtype,
|
|
39
|
-
|
|
52
|
+
make_datakey,
|
|
40
53
|
wait_for_connection,
|
|
41
54
|
)
|
|
55
|
+
from ophyd_async.tango.testing import TestConfig
|
|
42
56
|
|
|
43
57
|
from ._converters import (
|
|
44
58
|
TangoConverter,
|
|
@@ -47,7 +61,7 @@ from ._converters import (
|
|
|
47
61
|
TangoEnumArrayConverter,
|
|
48
62
|
TangoEnumConverter,
|
|
49
63
|
)
|
|
50
|
-
from ._utils import DevStateEnum, get_device_trl_and_attr
|
|
64
|
+
from ._utils import DevStateEnum, get_device_trl_and_attr, try_to_cast_as_float
|
|
51
65
|
|
|
52
66
|
logger = logging.getLogger("ophyd_async")
|
|
53
67
|
|
|
@@ -73,28 +87,85 @@ def ensure_proper_executor(
|
|
|
73
87
|
return wrapper
|
|
74
88
|
|
|
75
89
|
|
|
76
|
-
|
|
90
|
+
class TangoLongStringTable(Table):
|
|
91
|
+
long: Array1D[np.int32]
|
|
92
|
+
string: Sequence[str]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TangoDoubleStringTable(Table):
|
|
96
|
+
double: Array1D[np.float64]
|
|
97
|
+
string: Sequence[str]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_python_type(config: AttributeInfoEx | CommandInfo | TestConfig) -> object:
|
|
77
101
|
"""For converting between recieved tango types and python primatives."""
|
|
78
|
-
|
|
102
|
+
tango_type = None
|
|
103
|
+
tango_format = None
|
|
104
|
+
if isinstance(config, AttributeInfoEx | AttributeInfo):
|
|
105
|
+
tango_type = config.data_type
|
|
106
|
+
tango_format = config.data_format
|
|
107
|
+
elif isinstance(config, CommandInfo):
|
|
108
|
+
read_character = get_command_character(config)
|
|
109
|
+
if read_character == CommandProxyReadCharacter.READ:
|
|
110
|
+
tango_type = config.out_type
|
|
111
|
+
else:
|
|
112
|
+
tango_type = config.in_type
|
|
113
|
+
elif isinstance(config, TestConfig):
|
|
114
|
+
tango_type = config.data_type
|
|
115
|
+
tango_format = config.data_format
|
|
116
|
+
else:
|
|
117
|
+
raise TypeError("Unrecognized Tango resource configuration")
|
|
118
|
+
if tango_format not in [
|
|
119
|
+
AttrDataFormat.SCALAR,
|
|
120
|
+
AttrDataFormat.SPECTRUM,
|
|
121
|
+
AttrDataFormat.IMAGE,
|
|
122
|
+
None,
|
|
123
|
+
]:
|
|
124
|
+
raise TypeError("Unknown TangoFormat")
|
|
125
|
+
|
|
126
|
+
if tango_type is CmdArgType.DevVarLongStringArray:
|
|
127
|
+
return TangoLongStringTable
|
|
128
|
+
if tango_type is CmdArgType.DevVarDoubleStringArray:
|
|
129
|
+
return TangoDoubleStringTable
|
|
130
|
+
|
|
131
|
+
def _get_type(cls: type) -> object:
|
|
132
|
+
if tango_format == AttrDataFormat.SCALAR:
|
|
133
|
+
return cls
|
|
134
|
+
elif tango_format == AttrDataFormat.SPECTRUM:
|
|
135
|
+
if cls is str or issubclass(cls, StrictEnum):
|
|
136
|
+
return Sequence[cls]
|
|
137
|
+
return Array1D[cls]
|
|
138
|
+
elif tango_format == AttrDataFormat.IMAGE:
|
|
139
|
+
if cls is str or issubclass(cls, StrictEnum):
|
|
140
|
+
return Sequence[Sequence[str]]
|
|
141
|
+
return npt.NDArray[cls]
|
|
142
|
+
else:
|
|
143
|
+
return cls
|
|
144
|
+
|
|
79
145
|
if is_int(tango_type, True):
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
return
|
|
83
|
-
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return
|
|
97
|
-
|
|
146
|
+
return _get_type(int)
|
|
147
|
+
elif is_float(tango_type, True):
|
|
148
|
+
return _get_type(float)
|
|
149
|
+
elif is_bool(tango_type, True):
|
|
150
|
+
return _get_type(bool)
|
|
151
|
+
elif is_str(tango_type, True):
|
|
152
|
+
return _get_type(str)
|
|
153
|
+
elif is_binary(tango_type, True):
|
|
154
|
+
return _get_type(str)
|
|
155
|
+
elif tango_type == CmdArgType.DevEnum:
|
|
156
|
+
if hasattr(config, "enum_labels"):
|
|
157
|
+
enum_dict = {label: str(label) for label in config.enum_labels}
|
|
158
|
+
return _get_type(StrictEnum("TangoEnum", enum_dict))
|
|
159
|
+
else:
|
|
160
|
+
return _get_type(int)
|
|
161
|
+
elif tango_type == CmdArgType.DevState:
|
|
162
|
+
return _get_type(DevStateEnum)
|
|
163
|
+
elif tango_type == CmdArgType.DevUChar:
|
|
164
|
+
return _get_type(int)
|
|
165
|
+
elif tango_type == CmdArgType.DevVoid:
|
|
166
|
+
return None
|
|
167
|
+
else:
|
|
168
|
+
raise TypeError(f"Unknown TangoType: {tango_type}")
|
|
98
169
|
|
|
99
170
|
|
|
100
171
|
class TangoProxy:
|
|
@@ -202,49 +273,26 @@ class AttributeProxy(TangoProxy):
|
|
|
202
273
|
async def put( # type: ignore
|
|
203
274
|
self, value: object | None, wait: bool = True, timeout: float | None = None
|
|
204
275
|
) -> AsyncStatus | None:
|
|
276
|
+
if wait is False:
|
|
277
|
+
raise RuntimeWarning(
|
|
278
|
+
"wait=False is not supported in Tango."
|
|
279
|
+
"Simply don't await the status object."
|
|
280
|
+
)
|
|
205
281
|
# TODO: remove the timeout from this as it is handled at the signal level
|
|
206
282
|
value = self._converter.write_value(value)
|
|
207
|
-
|
|
208
|
-
try:
|
|
209
|
-
|
|
210
|
-
async def _write():
|
|
211
|
-
return await self._proxy.write_attribute(self._name, value)
|
|
212
|
-
|
|
213
|
-
task = asyncio.create_task(_write())
|
|
214
|
-
await asyncio.wait_for(task, timeout)
|
|
215
|
-
except TimeoutError as te:
|
|
216
|
-
raise TimeoutError(f"{self._name} attr put failed: Timeout") from te
|
|
217
|
-
except DevFailed as de:
|
|
218
|
-
raise RuntimeError(
|
|
219
|
-
f"{self._name} device failure: {de.args[0].desc}"
|
|
220
|
-
) from de
|
|
221
|
-
|
|
222
|
-
else:
|
|
223
|
-
rid = await self._proxy.write_attribute_asynch(self._name, value)
|
|
283
|
+
try:
|
|
224
284
|
|
|
225
|
-
async def
|
|
226
|
-
|
|
227
|
-
while True:
|
|
228
|
-
try:
|
|
229
|
-
# I have to typehint proxy as tango.DeviceProxy because
|
|
230
|
-
# tango.asyncio.DeviceProxy cannot be used as a typehint.
|
|
231
|
-
# This means pyright will not be able to see that
|
|
232
|
-
# write_attribute_reply is awaitable.
|
|
233
|
-
await self._proxy.write_attribute_reply(rd) # type: ignore
|
|
234
|
-
break
|
|
235
|
-
except DevFailed as exc:
|
|
236
|
-
if exc.args[0].reason == "API_AsynReplyNotArrived":
|
|
237
|
-
await asyncio.sleep(A_BIT)
|
|
238
|
-
if to and (time.time() - start_time > to):
|
|
239
|
-
raise TimeoutError(
|
|
240
|
-
f"{self._name} attr put failed: Timeout"
|
|
241
|
-
) from exc
|
|
242
|
-
else:
|
|
243
|
-
raise RuntimeError(
|
|
244
|
-
f"{self._name} device failure: {exc.args[0].desc}"
|
|
245
|
-
) from exc
|
|
285
|
+
async def _write():
|
|
286
|
+
return await self._proxy.write_attribute(self._name, value)
|
|
246
287
|
|
|
247
|
-
|
|
288
|
+
task = asyncio.create_task(_write())
|
|
289
|
+
await asyncio.wait_for(task, timeout)
|
|
290
|
+
except TimeoutError as te:
|
|
291
|
+
raise TimeoutError(f"{self._name} attr put failed: Timeout") from te
|
|
292
|
+
except DevFailed as de:
|
|
293
|
+
raise RuntimeError(
|
|
294
|
+
f"{self._name} device failure: {de.args[0].desc}"
|
|
295
|
+
) from de
|
|
248
296
|
|
|
249
297
|
@ensure_proper_executor
|
|
250
298
|
async def get_config(self) -> AttributeInfoEx: # type: ignore
|
|
@@ -264,6 +312,17 @@ class AttributeProxy(TangoProxy):
|
|
|
264
312
|
def has_subscription(self) -> bool:
|
|
265
313
|
return bool(self._callback)
|
|
266
314
|
|
|
315
|
+
@ensure_proper_executor
|
|
316
|
+
async def _subscribe_to_event(self):
|
|
317
|
+
if not self._eid:
|
|
318
|
+
self._eid = await self._proxy.subscribe_event(
|
|
319
|
+
self._name,
|
|
320
|
+
EventType.CHANGE_EVENT,
|
|
321
|
+
self._event_processor,
|
|
322
|
+
stateless=True,
|
|
323
|
+
green_mode=GreenMode.Asyncio,
|
|
324
|
+
)
|
|
325
|
+
|
|
267
326
|
def subscribe_callback(self, callback: Callback | None):
|
|
268
327
|
# If the attribute supports events, then we can subscribe to them
|
|
269
328
|
# If the callback is not a callable, then we raise an error
|
|
@@ -272,14 +331,7 @@ class AttributeProxy(TangoProxy):
|
|
|
272
331
|
|
|
273
332
|
self._callback = callback
|
|
274
333
|
if self.support_events:
|
|
275
|
-
|
|
276
|
-
if not self._eid:
|
|
277
|
-
self._eid = self._proxy.subscribe_event(
|
|
278
|
-
self._name,
|
|
279
|
-
EventType.CHANGE_EVENT,
|
|
280
|
-
self._event_processor,
|
|
281
|
-
green_mode=False,
|
|
282
|
-
)
|
|
334
|
+
asyncio.create_task(self._subscribe_to_event())
|
|
283
335
|
elif self._allow_polling:
|
|
284
336
|
"""start polling if no events supported"""
|
|
285
337
|
if self._callback is not None:
|
|
@@ -303,8 +355,12 @@ class AttributeProxy(TangoProxy):
|
|
|
303
355
|
|
|
304
356
|
def unsubscribe_callback(self):
|
|
305
357
|
if self._eid:
|
|
306
|
-
|
|
307
|
-
|
|
358
|
+
try:
|
|
359
|
+
self._proxy.unsubscribe_event(self._eid, green_mode=False)
|
|
360
|
+
except Exception as exc:
|
|
361
|
+
logger.warning(f"Could not unsubscribe from event: {exc}")
|
|
362
|
+
finally:
|
|
363
|
+
self._eid = None
|
|
308
364
|
if self._poll_task:
|
|
309
365
|
self._poll_task.cancel()
|
|
310
366
|
self._poll_task = None
|
|
@@ -316,7 +372,8 @@ class AttributeProxy(TangoProxy):
|
|
|
316
372
|
pass
|
|
317
373
|
self._callback = None
|
|
318
374
|
|
|
319
|
-
|
|
375
|
+
@ensure_proper_executor
|
|
376
|
+
async def _event_processor(self, event):
|
|
320
377
|
if not event.err:
|
|
321
378
|
reading = Reading(
|
|
322
379
|
value=self._converter.value(event.attr_value.value),
|
|
@@ -339,8 +396,8 @@ class AttributeProxy(TangoProxy):
|
|
|
339
396
|
# Initial reading
|
|
340
397
|
if self._callback is not None:
|
|
341
398
|
self._callback(last_reading)
|
|
342
|
-
except Exception as
|
|
343
|
-
raise RuntimeError(f"Could not poll the attribute: {
|
|
399
|
+
except Exception as exc:
|
|
400
|
+
raise RuntimeError(f"Could not poll the attribute: {exc}") from exc
|
|
344
401
|
|
|
345
402
|
try:
|
|
346
403
|
# If the value is a number, we can check for changes
|
|
@@ -396,8 +453,8 @@ class AttributeProxy(TangoProxy):
|
|
|
396
453
|
else:
|
|
397
454
|
break
|
|
398
455
|
last_reading = reading.copy()
|
|
399
|
-
except Exception as
|
|
400
|
-
raise RuntimeError(f"Could not poll the attribute: {
|
|
456
|
+
except Exception as exc:
|
|
457
|
+
raise RuntimeError(f"Could not poll the attribute: {exc}") from exc
|
|
401
458
|
|
|
402
459
|
def set_polling(
|
|
403
460
|
self,
|
|
@@ -413,10 +470,46 @@ class AttributeProxy(TangoProxy):
|
|
|
413
470
|
self._rel_change = rel_change
|
|
414
471
|
|
|
415
472
|
|
|
473
|
+
class CommandProxyReadCharacter(StrictEnum):
|
|
474
|
+
"""Enum to carry the read/write character of the CommandProxy."""
|
|
475
|
+
|
|
476
|
+
READ = "READ"
|
|
477
|
+
WRITE = "WRITE"
|
|
478
|
+
READ_WRITE = "READ_WRITE"
|
|
479
|
+
EXECUTE = "EXECUTE"
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def get_command_character(config: CommandInfo) -> CommandProxyReadCharacter:
|
|
483
|
+
"""Return the command character for the given command config."""
|
|
484
|
+
in_type = config.in_type
|
|
485
|
+
out_type = config.out_type
|
|
486
|
+
if in_type == CmdArgType.DevVoid and out_type != CmdArgType.DevVoid:
|
|
487
|
+
read_character = CommandProxyReadCharacter.READ
|
|
488
|
+
elif in_type != CmdArgType.DevVoid and out_type == CmdArgType.DevVoid:
|
|
489
|
+
read_character = CommandProxyReadCharacter.WRITE
|
|
490
|
+
elif in_type == CmdArgType.DevVoid and out_type == CmdArgType.DevVoid:
|
|
491
|
+
read_character = CommandProxyReadCharacter.EXECUTE
|
|
492
|
+
else:
|
|
493
|
+
read_character = CommandProxyReadCharacter.READ_WRITE
|
|
494
|
+
return read_character
|
|
495
|
+
|
|
496
|
+
|
|
416
497
|
class CommandProxy(TangoProxy):
|
|
417
498
|
"""Tango proxy for commands."""
|
|
418
499
|
|
|
419
|
-
_last_reading: Reading
|
|
500
|
+
_last_reading: Reading
|
|
501
|
+
_last_w_value: Any
|
|
502
|
+
_config: CommandInfo
|
|
503
|
+
_read_character: CommandProxyReadCharacter
|
|
504
|
+
device_proxy: DeviceProxy
|
|
505
|
+
name: str
|
|
506
|
+
|
|
507
|
+
def __init__(self, device_proxy: DeviceProxy, name: str):
|
|
508
|
+
super().__init__(device_proxy, name)
|
|
509
|
+
self._last_reading = Reading(value=None, timestamp=0, alarm_severity=0)
|
|
510
|
+
self.device_proxy = device_proxy
|
|
511
|
+
self.name = name
|
|
512
|
+
self._last_w_value = None
|
|
420
513
|
|
|
421
514
|
def subscribe_callback(self, callback: Callback | None) -> None:
|
|
422
515
|
raise NotImplementedError("Cannot subscribe to commands")
|
|
@@ -425,78 +518,59 @@ class CommandProxy(TangoProxy):
|
|
|
425
518
|
raise NotImplementedError("Cannot unsubscribe from commands")
|
|
426
519
|
|
|
427
520
|
async def get(self) -> object:
|
|
428
|
-
|
|
521
|
+
if self._read_character == CommandProxyReadCharacter.READ_WRITE:
|
|
522
|
+
return self._last_reading["value"]
|
|
523
|
+
elif self._read_character == CommandProxyReadCharacter.READ:
|
|
524
|
+
await self.put(value=None, wait=True, timeout=None)
|
|
525
|
+
return self._last_reading["value"]
|
|
429
526
|
|
|
430
527
|
async def get_w_value(self) -> object:
|
|
431
|
-
return self.
|
|
528
|
+
return self._last_w_value
|
|
432
529
|
|
|
433
530
|
async def connect(self) -> None:
|
|
434
|
-
|
|
531
|
+
self._config = await self.device_proxy.get_command_config(self.name)
|
|
532
|
+
self._read_character = get_command_character(self._config)
|
|
435
533
|
|
|
436
534
|
@ensure_proper_executor
|
|
437
535
|
async def put( # type: ignore
|
|
438
536
|
self, value: object | None, wait: bool = True, timeout: float | None = None
|
|
439
537
|
) -> AsyncStatus | None:
|
|
538
|
+
if wait is False:
|
|
539
|
+
raise RuntimeError(
|
|
540
|
+
"wait=False is not supported in Tango."
|
|
541
|
+
" Simply don't await the status object."
|
|
542
|
+
)
|
|
440
543
|
value = self._converter.write_value(value)
|
|
441
|
-
|
|
442
|
-
try:
|
|
443
|
-
|
|
444
|
-
async def _put():
|
|
445
|
-
return await self._proxy.command_inout(self._name, value)
|
|
446
|
-
|
|
447
|
-
task = asyncio.create_task(_put())
|
|
448
|
-
val = await asyncio.wait_for(task, timeout)
|
|
449
|
-
self._last_reading = Reading(
|
|
450
|
-
value=self._converter.value(val),
|
|
451
|
-
timestamp=time.time(),
|
|
452
|
-
alarm_severity=0,
|
|
453
|
-
)
|
|
454
|
-
except TimeoutError as te:
|
|
455
|
-
raise TimeoutError(f"{self._name} command failed: Timeout") from te
|
|
456
|
-
except DevFailed as de:
|
|
457
|
-
raise RuntimeError(
|
|
458
|
-
f"{self._name} device failure: {de.args[0].desc}"
|
|
459
|
-
) from de
|
|
460
|
-
|
|
461
|
-
else:
|
|
462
|
-
rid = self._proxy.command_inout_asynch(self._name, value)
|
|
544
|
+
try:
|
|
463
545
|
|
|
464
|
-
async def
|
|
465
|
-
|
|
466
|
-
while True:
|
|
467
|
-
try:
|
|
468
|
-
reply_value = self._converter.value(
|
|
469
|
-
self._proxy.command_inout_reply(rd)
|
|
470
|
-
)
|
|
471
|
-
self._last_reading = Reading(
|
|
472
|
-
value=reply_value, timestamp=time.time(), alarm_severity=0
|
|
473
|
-
)
|
|
474
|
-
break
|
|
475
|
-
except DevFailed as de_exc:
|
|
476
|
-
if de_exc.args[0].reason == "API_AsynReplyNotArrived":
|
|
477
|
-
await asyncio.sleep(A_BIT)
|
|
478
|
-
if to and time.time() - start_time > to:
|
|
479
|
-
raise TimeoutError(
|
|
480
|
-
"Timeout while waiting for command reply"
|
|
481
|
-
) from de_exc
|
|
482
|
-
else:
|
|
483
|
-
raise RuntimeError(
|
|
484
|
-
f"{self._name} device failure: {de_exc.args[0].desc}"
|
|
485
|
-
) from de_exc
|
|
546
|
+
async def _put():
|
|
547
|
+
return await self._proxy.command_inout(self._name, value)
|
|
486
548
|
|
|
487
|
-
|
|
549
|
+
task = asyncio.create_task(_put())
|
|
550
|
+
val = await asyncio.wait_for(task, timeout)
|
|
551
|
+
self._last_w_value = value
|
|
552
|
+
self._last_reading = Reading(
|
|
553
|
+
value=self._converter.value(val),
|
|
554
|
+
timestamp=time.time(),
|
|
555
|
+
alarm_severity=0,
|
|
556
|
+
)
|
|
557
|
+
except TimeoutError as te:
|
|
558
|
+
raise TimeoutError(f"{self._name} command failed: Timeout") from te
|
|
559
|
+
except DevFailed as de:
|
|
560
|
+
raise RuntimeError(
|
|
561
|
+
f"{self._name} device failure: {de.args[0].desc}"
|
|
562
|
+
) from de
|
|
488
563
|
|
|
489
564
|
@ensure_proper_executor
|
|
490
565
|
async def get_config(self) -> CommandInfo: # type: ignore
|
|
491
566
|
return await self._proxy.get_command_config(self._name)
|
|
492
567
|
|
|
493
568
|
async def get_reading(self) -> Reading:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
return reading
|
|
569
|
+
if self._read_character == CommandProxyReadCharacter.READ:
|
|
570
|
+
await self.put(value=None, wait=True, timeout=None)
|
|
571
|
+
return self._last_reading
|
|
572
|
+
else:
|
|
573
|
+
return self._last_reading
|
|
500
574
|
|
|
501
575
|
def set_polling(
|
|
502
576
|
self,
|
|
@@ -518,101 +592,79 @@ def get_dtype_extended(datatype) -> object | None:
|
|
|
518
592
|
return dtype
|
|
519
593
|
|
|
520
594
|
|
|
521
|
-
def
|
|
522
|
-
datatype: type | None,
|
|
595
|
+
def get_source_metadata(
|
|
523
596
|
tango_resource: str,
|
|
524
|
-
tr_configs: dict[str, AttributeInfoEx
|
|
525
|
-
) ->
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
for tr_name, config in tr_configs.items():
|
|
597
|
+
tr_configs: dict[str, AttributeInfoEx],
|
|
598
|
+
) -> SignalMetadata:
|
|
599
|
+
metadata = {}
|
|
600
|
+
for _, config in tr_configs.items():
|
|
529
601
|
if isinstance(config, AttributeInfoEx):
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
else config.out_type
|
|
602
|
+
alarm_info = config.alarms
|
|
603
|
+
_limits = Limits(
|
|
604
|
+
control=LimitsRange(
|
|
605
|
+
low=try_to_cast_as_float(config.min_value),
|
|
606
|
+
high=try_to_cast_as_float(config.max_value),
|
|
607
|
+
),
|
|
608
|
+
warning=LimitsRange(
|
|
609
|
+
low=try_to_cast_as_float(alarm_info.min_warning),
|
|
610
|
+
high=try_to_cast_as_float(alarm_info.max_warning),
|
|
611
|
+
),
|
|
612
|
+
alarm=LimitsRange(
|
|
613
|
+
low=try_to_cast_as_float(alarm_info.min_alarm),
|
|
614
|
+
high=try_to_cast_as_float(alarm_info.max_alarm),
|
|
615
|
+
),
|
|
545
616
|
)
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
descr,
|
|
617
|
+
|
|
618
|
+
delta_t, delta_val = map(
|
|
619
|
+
try_to_cast_as_float, (alarm_info.delta_t, alarm_info.delta_val)
|
|
550
620
|
)
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
max_y: int = (
|
|
564
|
-
trl_config.max_dim_y
|
|
565
|
-
if hasattr(trl_config, "max_dim_y")
|
|
566
|
-
else np.iinfo(np.int32).max
|
|
567
|
-
)
|
|
568
|
-
# is_attr = hasattr(trl_config, "enum_labels")
|
|
569
|
-
# trl_choices = list(trl_config.enum_labels) if is_attr else []
|
|
570
|
-
|
|
571
|
-
if tr_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]:
|
|
572
|
-
# This is an array
|
|
573
|
-
if datatype:
|
|
574
|
-
# Check we wanted an array of this type
|
|
575
|
-
dtype = get_dtype_extended(datatype)
|
|
576
|
-
if not dtype:
|
|
577
|
-
raise TypeError(
|
|
578
|
-
f"{tango_resource} has type [{tr_dtype}] not {datatype.__name__}"
|
|
621
|
+
if isinstance(delta_t, float) and isinstance(delta_val, float):
|
|
622
|
+
limits_rds = RdsRange(
|
|
623
|
+
time_difference=delta_t,
|
|
624
|
+
value_difference=delta_val,
|
|
625
|
+
)
|
|
626
|
+
_limits["rds"] = limits_rds
|
|
627
|
+
# if only one of the two is set
|
|
628
|
+
elif isinstance(delta_t, float) ^ isinstance(delta_val, float):
|
|
629
|
+
logger.warning(
|
|
630
|
+
f"Both delta_t and delta_val should be set for {tango_resource} "
|
|
631
|
+
f"but only one is set. "
|
|
632
|
+
f"delta_t: {alarm_info.delta_t}, delta_val: {alarm_info.delta_val}"
|
|
579
633
|
)
|
|
580
|
-
if dtype != tr_dtype:
|
|
581
|
-
raise TypeError(f"{tango_resource} has type [{tr_dtype}] not [{dtype}]")
|
|
582
634
|
|
|
583
|
-
|
|
584
|
-
return DataKey(source=tango_resource, dtype="array", shape=[max_x])
|
|
585
|
-
elif tr_format == AttrDataFormat.IMAGE:
|
|
586
|
-
return DataKey(source=tango_resource, dtype="array", shape=[max_y, max_x])
|
|
635
|
+
_choices = list(config.enum_labels) if config.enum_labels else []
|
|
587
636
|
|
|
588
|
-
|
|
589
|
-
if tr_dtype in (Enum, CmdArgType.DevState):
|
|
590
|
-
# if tr_dtype == CmdArgType.DevState:
|
|
591
|
-
# trl_choices = list(DevState.names.keys())
|
|
637
|
+
tr_dtype = get_python_type(config)
|
|
592
638
|
|
|
593
|
-
if
|
|
594
|
-
|
|
595
|
-
raise TypeError(
|
|
596
|
-
f"{tango_resource} has type Enum not {datatype.__name__}"
|
|
597
|
-
)
|
|
598
|
-
# if tr_dtype == Enum and is_attr:
|
|
599
|
-
# if isinstance(datatype, DevState):
|
|
600
|
-
# choices = tuple(v.name for v in datatype)
|
|
601
|
-
# if set(choices) != set(trl_choices):
|
|
602
|
-
# raise TypeError(
|
|
603
|
-
# f"{tango_resource} has choices {trl_choices} "
|
|
604
|
-
# f"not {choices}"
|
|
605
|
-
# )
|
|
606
|
-
return DataKey(source=tango_resource, dtype="string", shape=[])
|
|
607
|
-
else:
|
|
608
|
-
if datatype and not issubclass(tr_dtype, datatype):
|
|
609
|
-
raise TypeError(
|
|
610
|
-
f"{tango_resource} has type {tr_dtype.__name__} "
|
|
611
|
-
f"not {datatype.__name__}"
|
|
612
|
-
)
|
|
613
|
-
return DataKey(source=tango_resource, dtype=tr_dtype_desc, shape=[])
|
|
639
|
+
if tr_dtype == CmdArgType.DevState:
|
|
640
|
+
_choices = list(DevState.names.keys())
|
|
614
641
|
|
|
615
|
-
|
|
642
|
+
_precision = None
|
|
643
|
+
if config.format:
|
|
644
|
+
try:
|
|
645
|
+
_precision = int(config.format.split(".")[1].split("f")[0])
|
|
646
|
+
except (ValueError, IndexError) as exc:
|
|
647
|
+
# If parsing config.format fails, _precision remains None.
|
|
648
|
+
logger.warning(
|
|
649
|
+
"Failed to parse precision from config.format: %s. Error: %s",
|
|
650
|
+
config.format,
|
|
651
|
+
exc,
|
|
652
|
+
)
|
|
653
|
+
no_limits = Limits(
|
|
654
|
+
control=LimitsRange(high=None, low=None),
|
|
655
|
+
warning=LimitsRange(high=None, low=None),
|
|
656
|
+
alarm=LimitsRange(high=None, low=None),
|
|
657
|
+
)
|
|
658
|
+
if _limits:
|
|
659
|
+
if _limits != no_limits:
|
|
660
|
+
metadata["limits"] = _limits
|
|
661
|
+
if _choices:
|
|
662
|
+
metadata["choices"] = _choices
|
|
663
|
+
if _precision:
|
|
664
|
+
metadata["precision"] = _precision
|
|
665
|
+
if config.unit:
|
|
666
|
+
metadata["units"] = config.unit
|
|
667
|
+
return SignalMetadata(**metadata)
|
|
616
668
|
|
|
617
669
|
|
|
618
670
|
async def get_tango_trl(
|
|
@@ -625,7 +677,6 @@ async def get_tango_trl(
|
|
|
625
677
|
trl_name = trl_name.lower()
|
|
626
678
|
if device_proxy is None:
|
|
627
679
|
device_proxy = await AsyncDeviceProxy(device_trl, timeout=timeout)
|
|
628
|
-
|
|
629
680
|
# all attributes can be always accessible with low register
|
|
630
681
|
if isinstance(device_proxy, DeviceProxy):
|
|
631
682
|
all_attrs = [
|
|
@@ -703,7 +754,6 @@ class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
|
703
754
|
write_trl: self.device_proxy,
|
|
704
755
|
}
|
|
705
756
|
self.trl_configs: dict[str, AttributeInfoEx] = {}
|
|
706
|
-
self.descriptor: DataKey = {} # type: ignore
|
|
707
757
|
self._polling: tuple[bool, float, float | None, float | None] = (
|
|
708
758
|
False,
|
|
709
759
|
0.1,
|
|
@@ -730,20 +780,131 @@ class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
|
730
780
|
def source(self, name: str, read: bool) -> str:
|
|
731
781
|
return self.read_trl if read else self.write_trl
|
|
732
782
|
|
|
783
|
+
def _type_match_ndarray(self, signal_type: type[SignalDatatypeT], tr_dtype: object):
|
|
784
|
+
tango_resource = self.source(name="", read=True)
|
|
785
|
+
|
|
786
|
+
def extract_dtype_param(dtype_arg):
|
|
787
|
+
if hasattr(dtype_arg, "__origin__") and dtype_arg.__origin__ is np.dtype:
|
|
788
|
+
inner = get_args(dtype_arg)
|
|
789
|
+
return inner[0] if inner else object
|
|
790
|
+
return dtype_arg
|
|
791
|
+
|
|
792
|
+
signal_dtype = extract_dtype_param(get_args(signal_type)[-1])
|
|
793
|
+
tr_dtype_arg = extract_dtype_param(get_args(tr_dtype)[-1])
|
|
794
|
+
|
|
795
|
+
try:
|
|
796
|
+
sdt = np.dtype(signal_dtype)
|
|
797
|
+
tdt = np.dtype(tr_dtype_arg)
|
|
798
|
+
except TypeError as exc:
|
|
799
|
+
raise TypeError(
|
|
800
|
+
f"Could not interpret array dtypes: {signal_dtype!r},"
|
|
801
|
+
f" {tr_dtype_arg!r} ({exc})"
|
|
802
|
+
) from exc
|
|
803
|
+
|
|
804
|
+
if sdt != tdt:
|
|
805
|
+
raise TypeError(
|
|
806
|
+
f"{tango_resource} has type {tr_dtype!r}, expected {self.datatype!r}"
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
def _type_match_array(
|
|
810
|
+
self,
|
|
811
|
+
signal_type: type[SignalDatatypeT] | None,
|
|
812
|
+
tr_dtype: object,
|
|
813
|
+
tango_resource: str,
|
|
814
|
+
):
|
|
815
|
+
# Always get a fresh resource string for the error context
|
|
816
|
+
tango_resource = self.source(name="", read=True)
|
|
817
|
+
if get_origin(signal_type) is Sequence and get_origin(tr_dtype) is Sequence:
|
|
818
|
+
sig_elem_type = get_args(signal_type)[0]
|
|
819
|
+
tr_elem_type = get_args(tr_dtype)[0]
|
|
820
|
+
self._type_match_scalar(sig_elem_type, tr_elem_type, tango_resource)
|
|
821
|
+
return
|
|
822
|
+
elif (
|
|
823
|
+
get_origin(signal_type) is np.ndarray and get_origin(tr_dtype) is np.ndarray
|
|
824
|
+
):
|
|
825
|
+
if signal_type is None:
|
|
826
|
+
raise TypeError(
|
|
827
|
+
f"{tango_resource} has type {tr_dtype!r}, expected a non-None"
|
|
828
|
+
f" signal_type"
|
|
829
|
+
)
|
|
830
|
+
self._type_match_ndarray(signal_type, tr_dtype)
|
|
831
|
+
return
|
|
832
|
+
else:
|
|
833
|
+
raise TypeError(
|
|
834
|
+
tango_resource, "has type", str(signal_type), "which is not recognized"
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
def _type_match_scalar(
|
|
838
|
+
self,
|
|
839
|
+
signal_type: type[SignalDatatypeT] | None,
|
|
840
|
+
tr_dtype: object,
|
|
841
|
+
tango_resource: str,
|
|
842
|
+
):
|
|
843
|
+
if signal_type is tr_dtype:
|
|
844
|
+
return
|
|
845
|
+
if isinstance(signal_type, type) and issubclass(signal_type, StrictEnum):
|
|
846
|
+
return
|
|
847
|
+
raise TypeError(
|
|
848
|
+
f"{tango_resource} has type {tr_dtype!r}, expected {self.datatype!r}"
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
def _verify_datatype_matches(self, config: AttributeInfoEx | CommandInfo):
|
|
852
|
+
"""Verify that the datatype of the config matches the datatype of the signal."""
|
|
853
|
+
tr_dtype = get_python_type(config)
|
|
854
|
+
tango_resource = self.source(name="", read=True)
|
|
855
|
+
signal_type = self.datatype
|
|
856
|
+
if isinstance(config, AttributeInfoEx | AttributeInfo):
|
|
857
|
+
tr_format = config.data_format
|
|
858
|
+
if tr_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]:
|
|
859
|
+
self._type_match_array(signal_type, tr_dtype, tango_resource)
|
|
860
|
+
elif tr_format is AttrDataFormat.SCALAR:
|
|
861
|
+
self._type_match_scalar(signal_type, tr_dtype, tango_resource)
|
|
862
|
+
elif isinstance(config, CommandInfo):
|
|
863
|
+
if (
|
|
864
|
+
config.in_type != CmdArgType.DevVoid
|
|
865
|
+
and config.out_type != CmdArgType.DevVoid
|
|
866
|
+
and config.in_type != config.out_type
|
|
867
|
+
):
|
|
868
|
+
raise RuntimeError(
|
|
869
|
+
"Commands with different in and out dtypes are not supported"
|
|
870
|
+
)
|
|
871
|
+
if get_origin(tr_dtype) in [Sequence, np.ndarray]:
|
|
872
|
+
self._type_match_array(signal_type, tr_dtype, tango_resource)
|
|
873
|
+
else:
|
|
874
|
+
self._type_match_scalar(signal_type, tr_dtype, tango_resource)
|
|
875
|
+
else:
|
|
876
|
+
raise TypeError(
|
|
877
|
+
f"Unrecognized resource configuration: {config} "
|
|
878
|
+
f"for source {tango_resource}"
|
|
879
|
+
)
|
|
880
|
+
|
|
733
881
|
async def _connect_and_store_config(self, trl: str, timeout: float) -> None:
|
|
734
882
|
if not trl:
|
|
735
883
|
raise RuntimeError(f"trl not set for {self}")
|
|
736
884
|
try:
|
|
737
885
|
self.proxies[trl] = await get_tango_trl(trl, self.proxies[trl], timeout)
|
|
738
886
|
if self.proxies[trl] is None:
|
|
739
|
-
raise
|
|
887
|
+
raise NotConnectedError(f"Not connected to {trl}")
|
|
740
888
|
# Pyright does not believe that self.proxies[trl] is not None despite
|
|
741
889
|
# the check above
|
|
742
890
|
await self.proxies[trl].connect() # type: ignore
|
|
743
|
-
|
|
891
|
+
config = await self.proxies[trl].get_config() # type: ignore
|
|
892
|
+
self.trl_configs[trl] = config
|
|
893
|
+
|
|
894
|
+
# Perform signal verification
|
|
895
|
+
self._verify_datatype_matches(config)
|
|
896
|
+
|
|
897
|
+
if isinstance(config, AttributeInfoEx):
|
|
898
|
+
if (
|
|
899
|
+
config.data_type == CmdArgType.DevString
|
|
900
|
+
and config.data_format == AttrDataFormat.IMAGE
|
|
901
|
+
):
|
|
902
|
+
raise TypeError(
|
|
903
|
+
"DevString IMAGE attributes are not supported by ophyd-async."
|
|
904
|
+
)
|
|
744
905
|
self.proxies[trl].support_events = self.support_events # type: ignore
|
|
745
906
|
except TimeoutError as ce:
|
|
746
|
-
raise
|
|
907
|
+
raise NotConnectedError(f"tango://{trl}") from ce
|
|
747
908
|
|
|
748
909
|
async def connect(self, timeout: float) -> None:
|
|
749
910
|
if not self.read_trl:
|
|
@@ -760,47 +921,54 @@ class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
|
760
921
|
self.proxies[self.read_trl].set_polling(*self._polling) # type: ignore
|
|
761
922
|
self.converter = make_converter(self.trl_configs[self.read_trl], self.datatype)
|
|
762
923
|
self.proxies[self.read_trl].set_converter(self.converter) # type: ignore
|
|
763
|
-
self.descriptor = get_trl_descriptor(
|
|
764
|
-
self.datatype, self.read_trl, self.trl_configs
|
|
765
|
-
)
|
|
766
924
|
|
|
767
925
|
async def put(self, value: SignalDatatypeT | None, wait=True, timeout=None) -> None:
|
|
768
926
|
if self.proxies[self.write_trl] is None:
|
|
769
|
-
raise
|
|
927
|
+
raise NotConnectedError(f"Not connected to {self.write_trl}")
|
|
770
928
|
self.status = None
|
|
771
929
|
put_status = await self.proxies[self.write_trl].put(value, wait, timeout) # type: ignore
|
|
772
930
|
self.status = put_status
|
|
773
931
|
|
|
774
932
|
async def get_datakey(self, source: str) -> DataKey:
|
|
775
|
-
|
|
933
|
+
try:
|
|
934
|
+
value: Any = await self.proxies[source].get() # type: ignore
|
|
935
|
+
except AttributeError as ae:
|
|
936
|
+
raise NotConnectedError(f"Not connected to {source}") from ae
|
|
937
|
+
md = get_source_metadata(source, self.trl_configs)
|
|
938
|
+
return make_datakey(
|
|
939
|
+
self.datatype, # type: ignore
|
|
940
|
+
value,
|
|
941
|
+
source,
|
|
942
|
+
metadata=md,
|
|
943
|
+
)
|
|
776
944
|
|
|
777
945
|
async def get_reading(self) -> Reading[SignalDatatypeT]:
|
|
778
946
|
if self.proxies[self.read_trl] is None:
|
|
779
|
-
raise
|
|
947
|
+
raise NotConnectedError(f"Not connected to {self.read_trl}")
|
|
780
948
|
reading = await self.proxies[self.read_trl].get_reading() # type: ignore
|
|
781
949
|
return reading
|
|
782
950
|
|
|
783
951
|
async def get_value(self) -> SignalDatatypeT:
|
|
784
952
|
if self.proxies[self.read_trl] is None:
|
|
785
|
-
raise
|
|
953
|
+
raise NotConnectedError(f"Not connected to {self.read_trl}")
|
|
786
954
|
proxy = self.proxies[self.read_trl]
|
|
787
955
|
if proxy is None:
|
|
788
|
-
raise
|
|
956
|
+
raise NotConnectedError(f"Not connected to {self.read_trl}")
|
|
789
957
|
value = await proxy.get()
|
|
790
958
|
return cast(SignalDatatypeT, value)
|
|
791
959
|
|
|
792
960
|
async def get_setpoint(self) -> SignalDatatypeT:
|
|
793
961
|
if self.proxies[self.write_trl] is None:
|
|
794
|
-
raise
|
|
962
|
+
raise NotConnectedError(f"Not connected to {self.write_trl}")
|
|
795
963
|
proxy = self.proxies[self.write_trl]
|
|
796
964
|
if proxy is None:
|
|
797
|
-
raise
|
|
965
|
+
raise NotConnectedError(f"Not connected to {self.write_trl}")
|
|
798
966
|
w_value = await proxy.get_w_value()
|
|
799
967
|
return cast(SignalDatatypeT, w_value)
|
|
800
968
|
|
|
801
969
|
def set_callback(self, callback: Callback | None) -> None:
|
|
802
970
|
if self.proxies[self.read_trl] is None:
|
|
803
|
-
raise
|
|
971
|
+
raise NotConnectedError(f"Not connected to {self.read_trl}")
|
|
804
972
|
if self.support_events is False and self._polling[0] is False:
|
|
805
973
|
raise RuntimeError(
|
|
806
974
|
f"Cannot set event for {self.read_trl}. "
|