ophyd-async 0.13.6__py3-none-any.whl → 0.14.0__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 +28 -2
- ophyd_async/core/_device.py +92 -16
- ophyd_async/core/_mock_signal_backend.py +7 -1
- ophyd_async/{testing → core}/_mock_signal_utils.py +11 -14
- ophyd_async/core/_signal.py +3 -3
- ophyd_async/core/_utils.py +0 -40
- ophyd_async/epics/motor.py +55 -4
- ophyd_async/epics/pmac/_pmac_io.py +10 -13
- ophyd_async/epics/pmac/_pmac_trajectory.py +9 -10
- ophyd_async/epics/pmac/_utils.py +102 -59
- ophyd_async/tango/core/__init__.py +0 -2
- ophyd_async/tango/core/_base_device.py +4 -1
- ophyd_async/tango/demo/_counter.py +10 -3
- ophyd_async/tango/demo/_mover.py +4 -3
- ophyd_async/testing/__init__.py +27 -17
- {ophyd_async-0.13.6.dist-info → ophyd_async-0.14.0.dist-info}/METADATA +1 -1
- {ophyd_async-0.13.6.dist-info → ophyd_async-0.14.0.dist-info}/RECORD +21 -22
- ophyd_async/tango/core/_tango_readable.py +0 -15
- {ophyd_async-0.13.6.dist-info → ophyd_async-0.14.0.dist-info}/WHEEL +0 -0
- {ophyd_async-0.13.6.dist-info → ophyd_async-0.14.0.dist-info}/licenses/LICENSE +0 -0
- {ophyd_async-0.13.6.dist-info → ophyd_async-0.14.0.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.14.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 14, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
ophyd_async/core/__init__.py
CHANGED
|
@@ -14,7 +14,15 @@ from ._detector import (
|
|
|
14
14
|
StandardDetector,
|
|
15
15
|
TriggerInfo,
|
|
16
16
|
)
|
|
17
|
-
from ._device import
|
|
17
|
+
from ._device import (
|
|
18
|
+
Device,
|
|
19
|
+
DeviceConnector,
|
|
20
|
+
DeviceMock,
|
|
21
|
+
DeviceVector,
|
|
22
|
+
LazyMock,
|
|
23
|
+
default_mock_class,
|
|
24
|
+
init_devices,
|
|
25
|
+
)
|
|
18
26
|
from ._device_filler import DeviceFiller
|
|
19
27
|
from ._enums import (
|
|
20
28
|
EnabledDisabled,
|
|
@@ -27,6 +35,15 @@ from ._flyer import FlyerController, FlyMotorInfo, StandardFlyer
|
|
|
27
35
|
from ._hdf_dataset import HDFDatasetDescription, HDFDocumentComposer
|
|
28
36
|
from ._log import config_ophyd_async_logging
|
|
29
37
|
from ._mock_signal_backend import MockSignalBackend
|
|
38
|
+
from ._mock_signal_utils import (
|
|
39
|
+
callback_on_mock_put,
|
|
40
|
+
get_mock,
|
|
41
|
+
get_mock_put,
|
|
42
|
+
mock_puts_blocked,
|
|
43
|
+
set_mock_put_proceeds,
|
|
44
|
+
set_mock_value,
|
|
45
|
+
set_mock_values,
|
|
46
|
+
)
|
|
30
47
|
from ._protocol import AsyncConfigurable, AsyncReadable, AsyncStageable, Watcher
|
|
31
48
|
from ._providers import (
|
|
32
49
|
AutoIncrementFilenameProvider,
|
|
@@ -87,7 +104,6 @@ from ._utils import (
|
|
|
87
104
|
Callback,
|
|
88
105
|
ConfinedModel,
|
|
89
106
|
EnumTypes,
|
|
90
|
-
LazyMock,
|
|
91
107
|
NotConnectedError,
|
|
92
108
|
Reference,
|
|
93
109
|
StrictEnum,
|
|
@@ -166,8 +182,18 @@ __all__ = [
|
|
|
166
182
|
"soft_signal_r_and_setter",
|
|
167
183
|
"soft_signal_rw",
|
|
168
184
|
# Mock signal
|
|
185
|
+
"DeviceMock",
|
|
169
186
|
"LazyMock",
|
|
170
187
|
"MockSignalBackend",
|
|
188
|
+
"default_mock_class",
|
|
189
|
+
# Mocking utilities
|
|
190
|
+
"get_mock",
|
|
191
|
+
"set_mock_value",
|
|
192
|
+
"set_mock_values",
|
|
193
|
+
"get_mock_put",
|
|
194
|
+
"callback_on_mock_put",
|
|
195
|
+
"mock_puts_blocked",
|
|
196
|
+
"set_mock_put_proceeds",
|
|
171
197
|
# Signal utilities
|
|
172
198
|
"observe_value",
|
|
173
199
|
"observe_signals_value",
|
ophyd_async/core/_device.py
CHANGED
|
@@ -5,19 +5,71 @@ import sys
|
|
|
5
5
|
from collections.abc import Awaitable, Callable, Iterator, Mapping, MutableMapping
|
|
6
6
|
from functools import cached_property
|
|
7
7
|
from logging import LoggerAdapter, getLogger
|
|
8
|
-
from typing import Any, TypeVar
|
|
8
|
+
from typing import Any, Generic, TypeVar
|
|
9
|
+
from unittest.mock import Mock
|
|
9
10
|
|
|
10
11
|
from bluesky.protocols import HasName
|
|
11
12
|
from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
|
|
12
13
|
|
|
13
14
|
from ._utils import (
|
|
14
15
|
DEFAULT_TIMEOUT,
|
|
15
|
-
LazyMock,
|
|
16
16
|
NotConnectedError,
|
|
17
17
|
error_if_none,
|
|
18
18
|
wait_for_connection,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
+
DeviceT = TypeVar("DeviceT", bound="Device")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DeviceMock(Generic[DeviceT]):
|
|
25
|
+
"""A lazily created Mock to be used when connecting in mock mode.
|
|
26
|
+
|
|
27
|
+
Creating Mocks is reasonably expensive when each Device (and Signal)
|
|
28
|
+
requires its own, and the tree is only used when ``Signal.set()`` is
|
|
29
|
+
called. This class allows a tree of lazily connected Mocks to be
|
|
30
|
+
constructed so that when the leaf is created, so are its parents.
|
|
31
|
+
Any calls to the child are then accessible from the parent mock.
|
|
32
|
+
|
|
33
|
+
Subclasses can override the `connect()` method to inject custom logic
|
|
34
|
+
when mock devices are connected.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
>>> parent = DeviceMock()
|
|
38
|
+
>>> child = DeviceMock("child", parent)
|
|
39
|
+
>>> child_mock = child()
|
|
40
|
+
>>> child_mock() # doctest: +ELLIPSIS
|
|
41
|
+
<Mock name='mock.child()' id='...'>
|
|
42
|
+
>>> parent_mock = parent()
|
|
43
|
+
>>> parent_mock.mock_calls
|
|
44
|
+
[call.child()]
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, name: str = "", parent: DeviceMock | None = None) -> None:
|
|
50
|
+
self.name = name
|
|
51
|
+
self.parent = parent
|
|
52
|
+
self._mock: Mock | None = None
|
|
53
|
+
|
|
54
|
+
def __call__(self) -> Mock:
|
|
55
|
+
if self._mock is None:
|
|
56
|
+
self._mock = Mock(spec=object)
|
|
57
|
+
if self.parent is not None:
|
|
58
|
+
self.parent().attach_mock(self._mock, self.name)
|
|
59
|
+
return self._mock
|
|
60
|
+
|
|
61
|
+
async def connect(self, device: DeviceT) -> None:
|
|
62
|
+
"""Will be called when the device is connected in mock mode.
|
|
63
|
+
|
|
64
|
+
This allows mock values to be set and callbacks to be added
|
|
65
|
+
to the mock device so it behaves more like the real device.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Keep LazyMock as an alias for backwards compatibility
|
|
70
|
+
# Remove for ophyd-async 1.0
|
|
71
|
+
LazyMock = DeviceMock
|
|
72
|
+
|
|
21
73
|
|
|
22
74
|
class DeviceConnector:
|
|
23
75
|
"""Defines how a `Device` should be connected and type hints processed."""
|
|
@@ -40,7 +92,7 @@ class DeviceConnector:
|
|
|
40
92
|
during `__init__`.
|
|
41
93
|
"""
|
|
42
94
|
|
|
43
|
-
async def connect_mock(self, device: Device, mock:
|
|
95
|
+
async def connect_mock(self, device: Device, mock: DeviceMock):
|
|
44
96
|
"""Use during [](#Device.connect) with `mock=True`.
|
|
45
97
|
|
|
46
98
|
This is called when there is no cached connect done in `mock=True`
|
|
@@ -50,12 +102,16 @@ class DeviceConnector:
|
|
|
50
102
|
exceptions: dict[str, Exception] = {}
|
|
51
103
|
for name, child_device in device.children():
|
|
52
104
|
try:
|
|
53
|
-
|
|
105
|
+
child_mock_class = child_device._mock_class # noqa: SLF001
|
|
106
|
+
await child_device.connect(mock=child_mock_class(name, mock))
|
|
54
107
|
except Exception as exc:
|
|
55
108
|
exceptions[name] = exc
|
|
56
109
|
if exceptions:
|
|
57
110
|
raise NotConnectedError.with_other_exceptions_logged(exceptions)
|
|
58
111
|
|
|
112
|
+
# Call the DeviceMock's connect method to inject custom logic
|
|
113
|
+
await mock.connect(device)
|
|
114
|
+
|
|
59
115
|
async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
|
|
60
116
|
"""Use during [](#Device.connect) with `mock=False`.
|
|
61
117
|
|
|
@@ -82,8 +138,10 @@ class Device(HasName):
|
|
|
82
138
|
_name: str = ""
|
|
83
139
|
# None if connect hasn't started, a Task if it has
|
|
84
140
|
_connect_task: asyncio.Task | None = None
|
|
141
|
+
# The mock class to be used if we connect in mock mode
|
|
142
|
+
_mock_class: type[DeviceMock] = DeviceMock
|
|
85
143
|
# The mock if we have connected in mock mode
|
|
86
|
-
_mock:
|
|
144
|
+
_mock: DeviceMock | None = None
|
|
87
145
|
# The separator to use when making child names
|
|
88
146
|
_child_name_separator: str = "-"
|
|
89
147
|
|
|
@@ -163,7 +221,7 @@ class Device(HasName):
|
|
|
163
221
|
|
|
164
222
|
async def connect(
|
|
165
223
|
self,
|
|
166
|
-
mock: bool |
|
|
224
|
+
mock: bool | DeviceMock = False,
|
|
167
225
|
timeout: float = DEFAULT_TIMEOUT,
|
|
168
226
|
force_reconnect: bool = False,
|
|
169
227
|
) -> None:
|
|
@@ -175,25 +233,26 @@ class Device(HasName):
|
|
|
175
233
|
|
|
176
234
|
:param mock:
|
|
177
235
|
If True then use [](#MockSignalBackend) for all Signals. If passed a
|
|
178
|
-
[](#
|
|
179
|
-
otherwise create one
|
|
236
|
+
[](#DeviceMock) then pass this down for use within the Signals,
|
|
237
|
+
otherwise create one using the registered default mock for this device
|
|
238
|
+
type, or a plain [](#DeviceMock) if no default is registered.
|
|
180
239
|
:param timeout: Time to wait before failing with a TimeoutError.
|
|
181
240
|
:param force_reconnect:
|
|
182
241
|
If True, force a reconnect even if the last connect succeeded.
|
|
183
242
|
"""
|
|
184
|
-
connector = error_if_none(
|
|
243
|
+
connector: DeviceConnector = error_if_none(
|
|
185
244
|
getattr(self, "_connector", None),
|
|
186
245
|
f"{self}: doesn't have attribute `_connector`,"
|
|
187
246
|
f" did you call `super().__init__` in your `__init__` method?",
|
|
188
247
|
)
|
|
189
248
|
if mock:
|
|
190
249
|
# Always connect in mock mode serially
|
|
191
|
-
if isinstance(mock,
|
|
192
|
-
# Use the
|
|
250
|
+
if isinstance(mock, DeviceMock):
|
|
251
|
+
# Use the user supplied mock
|
|
193
252
|
self._mock = mock
|
|
194
253
|
elif not self._mock:
|
|
195
|
-
# Make
|
|
196
|
-
self._mock =
|
|
254
|
+
# Make a new mock of the registered type
|
|
255
|
+
self._mock = self._mock_class()
|
|
197
256
|
await connector.connect_mock(self, self._mock)
|
|
198
257
|
else:
|
|
199
258
|
# Try to cache the connect in real mode
|
|
@@ -223,9 +282,6 @@ _not_device_attrs = {
|
|
|
223
282
|
}
|
|
224
283
|
|
|
225
284
|
|
|
226
|
-
DeviceT = TypeVar("DeviceT", bound=Device)
|
|
227
|
-
|
|
228
|
-
|
|
229
285
|
class DeviceVector(MutableMapping[int, DeviceT], Device):
|
|
230
286
|
"""Defines a dictionary of Device children with arbitrary integer keys.
|
|
231
287
|
|
|
@@ -396,3 +452,23 @@ def init_devices(
|
|
|
396
452
|
await wait_for_connection(**coros)
|
|
397
453
|
|
|
398
454
|
return DeviceProcessor(process_devices)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def default_mock_class(
|
|
458
|
+
mock_cls: type[DeviceMock],
|
|
459
|
+
) -> Callable[[type[DeviceT]], type[DeviceT]]:
|
|
460
|
+
"""Register a DeviceMock subclass as the default mock for a Device class.
|
|
461
|
+
|
|
462
|
+
This decorator allows automatic injection of mock logic when devices are
|
|
463
|
+
connected in mock mode. The decorated DeviceMock class should override
|
|
464
|
+
the `connect()` method to define custom mock behavior.
|
|
465
|
+
|
|
466
|
+
:param mock_cls: A DeviceMock subclass to register.
|
|
467
|
+
:returns: A decorator that registers the mock class for a Device subclass.
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
def wrapper(device_cls: type[DeviceT]) -> type[DeviceT]:
|
|
471
|
+
device_cls._mock_class = mock_cls # noqa: SLF001
|
|
472
|
+
return device_cls
|
|
473
|
+
|
|
474
|
+
return wrapper
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
from collections.abc import Callable
|
|
3
5
|
from functools import cached_property
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
4
7
|
from unittest.mock import AsyncMock
|
|
5
8
|
|
|
6
9
|
from bluesky.protocols import Reading
|
|
@@ -9,7 +12,10 @@ from event_model import DataKey
|
|
|
9
12
|
from ._derived_signal_backend import DerivedSignalBackend
|
|
10
13
|
from ._signal_backend import SignalBackend, SignalDatatypeT
|
|
11
14
|
from ._soft_signal_backend import SoftSignalBackend
|
|
12
|
-
from ._utils import Callback
|
|
15
|
+
from ._utils import Callback
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ._device import LazyMock
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
class MockSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
@@ -2,15 +2,10 @@ from collections.abc import Awaitable, Callable, Iterable, Iterator
|
|
|
2
2
|
from contextlib import contextmanager
|
|
3
3
|
from unittest.mock import AsyncMock, Mock
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Signal,
|
|
10
|
-
SignalConnector,
|
|
11
|
-
SignalDatatypeT,
|
|
12
|
-
SignalR,
|
|
13
|
-
)
|
|
5
|
+
from ._device import Device, DeviceMock
|
|
6
|
+
from ._mock_signal_backend import MockSignalBackend
|
|
7
|
+
from ._signal import Signal, SignalConnector, SignalR
|
|
8
|
+
from ._signal_backend import SignalDatatypeT
|
|
14
9
|
|
|
15
10
|
|
|
16
11
|
def get_mock(device: Device | Signal) -> Mock:
|
|
@@ -19,16 +14,18 @@ def get_mock(device: Device | Signal) -> Mock:
|
|
|
19
14
|
The device must have been connected in mock mode.
|
|
20
15
|
"""
|
|
21
16
|
mock = device._mock # noqa: SLF001
|
|
22
|
-
|
|
17
|
+
if not isinstance(mock, DeviceMock):
|
|
18
|
+
msg = f"Device {device} not connected in mock mode"
|
|
19
|
+
raise RuntimeError(msg)
|
|
23
20
|
return mock()
|
|
24
21
|
|
|
25
22
|
|
|
26
23
|
def _get_mock_signal_backend(signal: Signal) -> MockSignalBackend:
|
|
27
24
|
connector = signal._connector # noqa: SLF001
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
if not isinstance(connector, SignalConnector):
|
|
26
|
+
raise TypeError(f"Expected Signal, got {signal}")
|
|
27
|
+
if not isinstance(connector.backend, MockSignalBackend):
|
|
28
|
+
raise RuntimeError(f"Signal {signal} not connected in mock mode")
|
|
32
29
|
return connector.backend
|
|
33
30
|
|
|
34
31
|
|
ophyd_async/core/_signal.py
CHANGED
|
@@ -19,7 +19,7 @@ from bluesky.protocols import (
|
|
|
19
19
|
from event_model import DataKey
|
|
20
20
|
from stamina import retry_context
|
|
21
21
|
|
|
22
|
-
from ._device import Device, DeviceConnector
|
|
22
|
+
from ._device import Device, DeviceConnector, LazyMock
|
|
23
23
|
from ._mock_signal_backend import MockSignalBackend
|
|
24
24
|
from ._protocol import AsyncReadable, AsyncStageable
|
|
25
25
|
from ._signal_backend import SignalBackend, SignalDatatypeT, SignalDatatypeV
|
|
@@ -30,7 +30,6 @@ from ._utils import (
|
|
|
30
30
|
DEFAULT_TIMEOUT,
|
|
31
31
|
CalculatableTimeout,
|
|
32
32
|
Callback,
|
|
33
|
-
LazyMock,
|
|
34
33
|
T,
|
|
35
34
|
error_if_none,
|
|
36
35
|
)
|
|
@@ -639,9 +638,10 @@ async def set_and_wait_for_other_value(
|
|
|
639
638
|
if wait_for_set_completion:
|
|
640
639
|
await status
|
|
641
640
|
except TimeoutError as exc:
|
|
641
|
+
matcher_name = getattr(matcher, "__name__", f"<{type(matcher).__name__}>")
|
|
642
642
|
raise TimeoutError(
|
|
643
643
|
f"{match_signal.name} value didn't match value from"
|
|
644
|
-
f" {
|
|
644
|
+
f" {matcher_name}() in {timeout}s"
|
|
645
645
|
) from exc
|
|
646
646
|
|
|
647
647
|
return status
|
ophyd_async/core/_utils.py
CHANGED
|
@@ -14,7 +14,6 @@ from typing import (
|
|
|
14
14
|
get_args,
|
|
15
15
|
get_origin,
|
|
16
16
|
)
|
|
17
|
-
from unittest.mock import Mock
|
|
18
17
|
|
|
19
18
|
import numpy as np
|
|
20
19
|
from pydantic import BaseModel, ConfigDict
|
|
@@ -342,45 +341,6 @@ class Reference(Generic[T]):
|
|
|
342
341
|
return self._obj
|
|
343
342
|
|
|
344
343
|
|
|
345
|
-
class LazyMock:
|
|
346
|
-
"""A lazily created Mock to be used when connecting in mock mode.
|
|
347
|
-
|
|
348
|
-
Creating Mocks is reasonably expensive when each Device (and Signal)
|
|
349
|
-
requires its own, and the tree is only used when ``Signal.set()`` is
|
|
350
|
-
called. This class allows a tree of lazily connected Mocks to be
|
|
351
|
-
constructed so that when the leaf is created, so are its parents.
|
|
352
|
-
Any calls to the child are then accessible from the parent mock.
|
|
353
|
-
|
|
354
|
-
```python
|
|
355
|
-
>>> parent = LazyMock()
|
|
356
|
-
>>> child = parent.child("child")
|
|
357
|
-
>>> child_mock = child()
|
|
358
|
-
>>> child_mock() # doctest: +ELLIPSIS
|
|
359
|
-
<Mock name='mock.child()' id='...'>
|
|
360
|
-
>>> parent_mock = parent()
|
|
361
|
-
>>> parent_mock.mock_calls
|
|
362
|
-
[call.child()]
|
|
363
|
-
|
|
364
|
-
```
|
|
365
|
-
"""
|
|
366
|
-
|
|
367
|
-
def __init__(self, name: str = "", parent: LazyMock | None = None) -> None:
|
|
368
|
-
self.parent = parent
|
|
369
|
-
self.name = name
|
|
370
|
-
self._mock: Mock | None = None
|
|
371
|
-
|
|
372
|
-
def child(self, name: str) -> LazyMock:
|
|
373
|
-
"""Return a child of this LazyMock with the given name."""
|
|
374
|
-
return LazyMock(name, self)
|
|
375
|
-
|
|
376
|
-
def __call__(self) -> Mock:
|
|
377
|
-
if self._mock is None:
|
|
378
|
-
self._mock = Mock(spec=object)
|
|
379
|
-
if self.parent is not None:
|
|
380
|
-
self.parent().attach_mock(self._mock, self.name)
|
|
381
|
-
return self._mock
|
|
382
|
-
|
|
383
|
-
|
|
384
344
|
class ConfinedModel(BaseModel):
|
|
385
345
|
"""A base class confined to explicitly defined fields in the model schema."""
|
|
386
346
|
|
ophyd_async/epics/motor.py
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
https://github.com/epics-modules/motor
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
6
8
|
import asyncio
|
|
7
9
|
|
|
8
10
|
from bluesky.protocols import (
|
|
@@ -21,18 +23,22 @@ from ophyd_async.core import (
|
|
|
21
23
|
AsyncStatus,
|
|
22
24
|
CalculatableTimeout,
|
|
23
25
|
Callback,
|
|
26
|
+
DeviceMock,
|
|
24
27
|
FlyMotorInfo,
|
|
25
28
|
StandardReadable,
|
|
26
29
|
StrictEnum,
|
|
27
30
|
WatchableAsyncStatus,
|
|
28
31
|
WatcherUpdate,
|
|
32
|
+
callback_on_mock_put,
|
|
33
|
+
default_mock_class,
|
|
29
34
|
error_if_none,
|
|
30
35
|
observe_value,
|
|
36
|
+
set_mock_value,
|
|
31
37
|
)
|
|
32
38
|
from ophyd_async.core import StandardReadableFormat as Format
|
|
33
39
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
|
|
34
40
|
|
|
35
|
-
__all__ = ["MotorLimitsError", "Motor"]
|
|
41
|
+
__all__ = ["MotorLimitsError", "Motor", "InstantMotorMock", "OffsetMode", "UseSetMode"]
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
class MotorLimitsError(Exception):
|
|
@@ -61,15 +67,45 @@ def __getattr__(name):
|
|
|
61
67
|
|
|
62
68
|
|
|
63
69
|
class OffsetMode(StrictEnum):
|
|
70
|
+
"""In Set mode, determine what to do when the motor setpoint is written."""
|
|
71
|
+
|
|
64
72
|
VARIABLE = "Variable"
|
|
73
|
+
"""Change the offset so the readback matches the setpoint."""
|
|
65
74
|
FROZEN = "Frozen"
|
|
75
|
+
"""Tell the controller to change the readback without changing the offset."""
|
|
66
76
|
|
|
67
77
|
|
|
68
78
|
class UseSetMode(StrictEnum):
|
|
79
|
+
"""Determine what to do when the motor setpoint is written."""
|
|
80
|
+
|
|
69
81
|
USE = "Use"
|
|
82
|
+
"""Tell the controller to move to the setpoint."""
|
|
70
83
|
SET = "Set"
|
|
84
|
+
"""Change offset (in record or in controller) when setpoint is written."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class InstantMotorMock(DeviceMock["Motor"]):
|
|
88
|
+
"""Mock behaviour that instantly moves readback to setpoint."""
|
|
89
|
+
|
|
90
|
+
async def connect(self, device: Motor) -> None:
|
|
91
|
+
"""Mock signals to do an instant move on setpoint write."""
|
|
92
|
+
# Set sensible defaults to avoid runtime errors
|
|
93
|
+
set_mock_value(device.velocity, 1000) # Prevent ZeroDivisionError
|
|
94
|
+
set_mock_value(device.max_velocity, 1000) # Prevent ZeroDivisionError
|
|
71
95
|
|
|
96
|
+
# Motor starts in "done" state (not moving)
|
|
97
|
+
set_mock_value(device.motor_done_move, 1)
|
|
72
98
|
|
|
99
|
+
# When setpoint is written to, immediately update readback and done flag
|
|
100
|
+
def _instant_move(value, wait):
|
|
101
|
+
set_mock_value(device.motor_done_move, 0) # Moving
|
|
102
|
+
set_mock_value(device.user_readback, value) # Arrive instantly
|
|
103
|
+
set_mock_value(device.motor_done_move, 1) # Done
|
|
104
|
+
|
|
105
|
+
callback_on_mock_put(device.user_setpoint, _instant_move)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@default_mock_class(InstantMotorMock)
|
|
73
109
|
class Motor(
|
|
74
110
|
StandardReadable,
|
|
75
111
|
Locatable[float],
|
|
@@ -98,9 +134,12 @@ class Motor(
|
|
|
98
134
|
self.motor_done_move = epics_signal_r(int, prefix + ".DMOV")
|
|
99
135
|
self.low_limit_travel = epics_signal_rw(float, prefix + ".LLM")
|
|
100
136
|
self.high_limit_travel = epics_signal_rw(float, prefix + ".HLM")
|
|
137
|
+
self.dial_low_limit_travel = epics_signal_rw(float, prefix + ".DLLM")
|
|
138
|
+
self.dial_high_limit_travel = epics_signal_rw(float, prefix + ".DHLM")
|
|
101
139
|
self.offset_freeze_switch = epics_signal_rw(OffsetMode, prefix + ".FOFF")
|
|
102
140
|
self.high_limit_switch = epics_signal_r(int, prefix + ".HLS")
|
|
103
141
|
self.low_limit_switch = epics_signal_r(int, prefix + ".LLS")
|
|
142
|
+
self.output_link = epics_signal_r(str, prefix + ".OUT")
|
|
104
143
|
self.set_use_switch = epics_signal_rw(UseSetMode, prefix + ".SET")
|
|
105
144
|
|
|
106
145
|
# Note:cannot use epics_signal_x here, as the motor record specifies that
|
|
@@ -131,16 +170,26 @@ class Motor(
|
|
|
131
170
|
Will raise a MotorLimitsException if the given absolute positions will be
|
|
132
171
|
outside the motor soft limits.
|
|
133
172
|
"""
|
|
134
|
-
|
|
173
|
+
(
|
|
174
|
+
motor_lower_limit,
|
|
175
|
+
motor_upper_limit,
|
|
176
|
+
egu,
|
|
177
|
+
dial_lower_limit,
|
|
178
|
+
dial_upper_limit,
|
|
179
|
+
) = await asyncio.gather(
|
|
135
180
|
self.low_limit_travel.get_value(),
|
|
136
181
|
self.high_limit_travel.get_value(),
|
|
137
182
|
self.motor_egu.get_value(),
|
|
183
|
+
self.dial_low_limit_travel.get_value(),
|
|
184
|
+
self.dial_high_limit_travel.get_value(),
|
|
138
185
|
)
|
|
139
186
|
|
|
140
|
-
# EPICS motor record treats limits of 0, 0 as no limit
|
|
141
|
-
|
|
187
|
+
# EPICS motor record treats dial limits of 0, 0 as no limit
|
|
188
|
+
# Use DLLM and DHLM to check
|
|
189
|
+
if dial_lower_limit == 0 and dial_upper_limit == 0:
|
|
142
190
|
return
|
|
143
191
|
|
|
192
|
+
# Use real motor limit(i.e. HLM and LLM) to check if the move is permissible
|
|
144
193
|
if (
|
|
145
194
|
not motor_upper_limit >= abs_start_pos >= motor_lower_limit
|
|
146
195
|
or not motor_upper_limit >= abs_end_pos >= motor_lower_limit
|
|
@@ -150,6 +199,8 @@ class Motor(
|
|
|
150
199
|
f"{abs_start_pos}{egu} to "
|
|
151
200
|
f"{abs_end_pos}{egu} but motor limits are "
|
|
152
201
|
f"{motor_lower_limit}{egu} <= x <= {motor_upper_limit}{egu} "
|
|
202
|
+
f"dial limits are "
|
|
203
|
+
f"{dial_lower_limit}{egu} <= x <= {dial_upper_limit}"
|
|
153
204
|
)
|
|
154
205
|
|
|
155
206
|
@AsyncStatus.wrap
|
|
@@ -6,7 +6,8 @@ from ophyd_async.core import Array1D, Device, DeviceVector, StandardReadable
|
|
|
6
6
|
from ophyd_async.epics import motor
|
|
7
7
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
# Map the CS axis letters to their index (1 indexed)
|
|
10
|
+
CS_INDEX = {letter: index + 1 for index, letter in enumerate("ABCUVWXYZ")}
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class PmacTrajectoryIO(StandardReadable):
|
|
@@ -20,24 +21,20 @@ class PmacTrajectoryIO(StandardReadable):
|
|
|
20
21
|
# 1 indexed CS axes so we can index into them from the compound motor input link
|
|
21
22
|
self.positions = DeviceVector(
|
|
22
23
|
{
|
|
23
|
-
i
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
for i, letter in enumerate(CS_LETTERS)
|
|
24
|
+
i: epics_signal_rw(Array1D[np.float64], f"{prefix}{letter}:Positions")
|
|
25
|
+
for letter, i in CS_INDEX.items()
|
|
27
26
|
}
|
|
28
27
|
)
|
|
29
28
|
self.use_axis = DeviceVector(
|
|
30
29
|
{
|
|
31
|
-
i
|
|
32
|
-
for
|
|
30
|
+
i: epics_signal_rw(bool, f"{prefix}{letter}:UseAxis")
|
|
31
|
+
for letter, i in CS_INDEX.items()
|
|
33
32
|
}
|
|
34
33
|
)
|
|
35
34
|
self.velocities = DeviceVector(
|
|
36
35
|
{
|
|
37
|
-
i
|
|
38
|
-
|
|
39
|
-
)
|
|
40
|
-
for i, letter in enumerate(CS_LETTERS)
|
|
36
|
+
i: epics_signal_rw(Array1D[np.float64], f"{prefix}{letter}:Velocities")
|
|
37
|
+
for letter, i in CS_INDEX.items()
|
|
41
38
|
}
|
|
42
39
|
)
|
|
43
40
|
self.total_points = epics_signal_r(int, f"{prefix}TotalPoints_RBV")
|
|
@@ -76,8 +73,8 @@ class PmacCoordIO(Device):
|
|
|
76
73
|
self.cs_port = epics_signal_r(str, f"{prefix}Port")
|
|
77
74
|
self.cs_axis_setpoint = DeviceVector(
|
|
78
75
|
{
|
|
79
|
-
i
|
|
80
|
-
for i in
|
|
76
|
+
i: epics_signal_rw(float, f"{prefix}M{i}:DirectDemand")
|
|
77
|
+
for i in CS_INDEX.values()
|
|
81
78
|
}
|
|
82
79
|
)
|
|
83
80
|
super().__init__(name=name)
|
|
@@ -15,10 +15,10 @@ from ophyd_async.core import (
|
|
|
15
15
|
wait_for_value,
|
|
16
16
|
)
|
|
17
17
|
from ophyd_async.epics.motor import Motor
|
|
18
|
-
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from
|
|
18
|
+
|
|
19
|
+
from ._pmac_io import CS_INDEX, PmacIO
|
|
20
|
+
from ._pmac_trajectory_generation import PVT, Trajectory
|
|
21
|
+
from ._utils import (
|
|
22
22
|
_PmacMotorInfo,
|
|
23
23
|
calculate_ramp_position_and_duration,
|
|
24
24
|
)
|
|
@@ -131,8 +131,7 @@ class PmacTrajectoryTriggerLogic(FlyerController):
|
|
|
131
131
|
slice, path_length, motor_info, ramp_up_time
|
|
132
132
|
)
|
|
133
133
|
use_axis = {
|
|
134
|
-
|
|
135
|
-
for axis in range(len(CS_LETTERS))
|
|
134
|
+
i: (i in motor_info.motor_cs_index.values()) for i in CS_INDEX.values()
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
coros = [
|
|
@@ -177,14 +176,14 @@ class PmacTrajectoryTriggerLogic(FlyerController):
|
|
|
177
176
|
self, trajectory: Trajectory, motor_info: _PmacMotorInfo
|
|
178
177
|
):
|
|
179
178
|
coros = []
|
|
180
|
-
for motor,
|
|
179
|
+
for motor, cs_index in motor_info.motor_cs_index.items():
|
|
181
180
|
coros.append(
|
|
182
|
-
self.pmac.trajectory.positions[
|
|
181
|
+
self.pmac.trajectory.positions[cs_index].set(
|
|
183
182
|
trajectory.positions[motor]
|
|
184
183
|
)
|
|
185
184
|
)
|
|
186
185
|
coros.append(
|
|
187
|
-
self.pmac.trajectory.velocities[
|
|
186
|
+
self.pmac.trajectory.velocities[cs_index].set(
|
|
188
187
|
trajectory.velocities[motor]
|
|
189
188
|
)
|
|
190
189
|
)
|
|
@@ -206,7 +205,7 @@ class PmacTrajectoryTriggerLogic(FlyerController):
|
|
|
206
205
|
for motor, position in ramp_up_position.items():
|
|
207
206
|
coros.append(
|
|
208
207
|
set_and_wait_for_value(
|
|
209
|
-
coord.cs_axis_setpoint[motor_info.motor_cs_index[motor]
|
|
208
|
+
coord.cs_axis_setpoint[motor_info.motor_cs_index[motor]],
|
|
210
209
|
position,
|
|
211
210
|
set_timeout=10,
|
|
212
211
|
wait_for_set_completion=False,
|
ophyd_async/epics/pmac/_utils.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import re
|
|
4
5
|
from collections.abc import Sequence
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
|
|
@@ -10,7 +11,7 @@ from scanspec.core import Slice
|
|
|
10
11
|
from ophyd_async.core import gather_dict
|
|
11
12
|
from ophyd_async.epics.motor import Motor
|
|
12
13
|
|
|
13
|
-
from ._pmac_io import
|
|
14
|
+
from ._pmac_io import CS_INDEX, PmacIO
|
|
14
15
|
|
|
15
16
|
# PMAC durations are in milliseconds
|
|
16
17
|
# We must convert from scanspec durations (seconds) to milliseconds
|
|
@@ -21,6 +22,11 @@ TICK_S = 0.000001
|
|
|
21
22
|
MIN_TURNAROUND = 0.002
|
|
22
23
|
MIN_INTERVAL = 0.002
|
|
23
24
|
|
|
25
|
+
# Regex to parse outlink strings of the form "@asyn(PMAC1CS2, 7)"
|
|
26
|
+
# returning PMAC1CS2 and 7
|
|
27
|
+
# https://regex101.com/r/Mu9XpO/1
|
|
28
|
+
OUTLINK_REGEX = re.compile(r"^\@asyn\(([^,]+),\s*(\d+)\)$")
|
|
29
|
+
|
|
24
30
|
|
|
25
31
|
@dataclass
|
|
26
32
|
class _PmacMotorInfo:
|
|
@@ -44,78 +50,115 @@ class _PmacMotorInfo:
|
|
|
44
50
|
dictionaries of motor's to their unique CS index and accelerate rate
|
|
45
51
|
|
|
46
52
|
"""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
for
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
53
|
+
is_raw_motor = [motor in pmac.motor_assignment_index for motor in motors]
|
|
54
|
+
if all(is_raw_motor):
|
|
55
|
+
# Get the CS port, number and axis letter from the PVs for the raw motor
|
|
56
|
+
assignments = {
|
|
57
|
+
motor: pmac.assignment[pmac.motor_assignment_index[motor]]
|
|
58
|
+
for motor in motors
|
|
59
|
+
}
|
|
60
|
+
cs_ports, cs_numbers, cs_axis_letters = await asyncio.gather(
|
|
61
|
+
gather_dict(
|
|
62
|
+
{motor: assignments[motor].cs_port.get_value() for motor in motors}
|
|
63
|
+
),
|
|
64
|
+
gather_dict(
|
|
65
|
+
{
|
|
66
|
+
motor: assignments[motor].cs_number.get_value()
|
|
67
|
+
for motor in motors
|
|
68
|
+
}
|
|
69
|
+
),
|
|
70
|
+
gather_dict(
|
|
71
|
+
{
|
|
72
|
+
motor: assignments[motor].cs_axis_letter.get_value()
|
|
73
|
+
for motor in motors
|
|
74
|
+
}
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
# Translate axis letters to cs_index and check for duplicates
|
|
78
|
+
motor_cs_index: dict[Motor, int] = {}
|
|
79
|
+
for motor, cs_axis_letter in cs_axis_letters.items():
|
|
80
|
+
if not cs_axis_letter:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Motor {motor.name} does not have an axis assignment."
|
|
83
|
+
)
|
|
84
|
+
try:
|
|
85
|
+
# 1 indexed to match coord
|
|
86
|
+
index = CS_INDEX[cs_axis_letter]
|
|
87
|
+
except KeyError as err:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"Motor {motor.name} assigned to '{cs_axis_letter}' "
|
|
90
|
+
f"but must be assigned to one of '{','.join(CS_INDEX)}'"
|
|
91
|
+
) from err
|
|
92
|
+
if index in motor_cs_index.values():
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"Motor {motor.name} assigned to '{cs_axis_letter}' "
|
|
95
|
+
"but another motor is already assigned to this axis."
|
|
96
|
+
)
|
|
97
|
+
motor_cs_index[motor] = index
|
|
98
|
+
elif not any(is_raw_motor):
|
|
99
|
+
# Get CS numbers from all the cs ports and output links for the CS motors
|
|
100
|
+
output_links, cs_lookup = await asyncio.gather(
|
|
101
|
+
gather_dict({motor: motor.output_link.get_value() for motor in motors}),
|
|
102
|
+
gather_dict(
|
|
103
|
+
{
|
|
104
|
+
cs_number: cs.cs_port.get_value()
|
|
105
|
+
for cs_number, cs in pmac.coord.items()
|
|
106
|
+
}
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
# Create a reverse lookup from cs_port to cs_number
|
|
110
|
+
cs_reverse_lookup = {
|
|
111
|
+
cs_port: cs_number for cs_number, cs_port in cs_lookup.items()
|
|
112
|
+
}
|
|
113
|
+
cs_ports: dict[Motor, str] = {}
|
|
114
|
+
cs_numbers: dict[Motor, int] = {}
|
|
115
|
+
motor_cs_index: dict[Motor, int] = {}
|
|
116
|
+
# Populate the cs_ports, cs_numbers and motor_cs_index dicts from outlinks
|
|
117
|
+
for motor, output_link in output_links.items():
|
|
118
|
+
match = OUTLINK_REGEX.match(output_link)
|
|
119
|
+
if not match:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"Motor {motor.name} has invalid output link '{output_link}'"
|
|
122
|
+
)
|
|
123
|
+
cs_port, cs_index = match.groups()
|
|
124
|
+
cs_ports[motor] = cs_port
|
|
125
|
+
cs_numbers[motor] = cs_reverse_lookup[cs_port]
|
|
126
|
+
motor_cs_index[motor] = int(cs_index)
|
|
127
|
+
else:
|
|
128
|
+
raise ValueError("Unable to use raw motors and CS motors in the same scan")
|
|
70
129
|
|
|
71
130
|
# check if the values in cs_port and cs_number are the same
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if len(cs_ports) != 1:
|
|
131
|
+
cs_ports_set = set(cs_ports.values())
|
|
132
|
+
if len(cs_ports_set) != 1:
|
|
75
133
|
raise RuntimeError(
|
|
76
134
|
"Failed to fetch common CS port."
|
|
77
135
|
"Motors passed are assigned to multiple CS ports:"
|
|
78
|
-
f"{list(
|
|
136
|
+
f"{list(cs_ports_set)}"
|
|
79
137
|
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
cs_numbers = set(cs_numbers.values())
|
|
84
|
-
if len(cs_numbers) != 1:
|
|
138
|
+
cs_numbers_set = set(cs_numbers.values())
|
|
139
|
+
if len(cs_numbers_set) != 1:
|
|
85
140
|
raise RuntimeError(
|
|
86
141
|
"Failed to fetch common CS number."
|
|
87
142
|
"Motors passed are assigned to multiple CS numbers:"
|
|
88
|
-
f"{list(
|
|
143
|
+
f"{list(cs_numbers_set)}"
|
|
89
144
|
)
|
|
90
145
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
motor_cs_index[motor] = CS_LETTERS.index(cs_axes[motor])
|
|
99
|
-
except ValueError as err:
|
|
100
|
-
raise ValueError(
|
|
101
|
-
"Failed to get motor CS index. "
|
|
102
|
-
f"Motor {motor.name} assigned to '{cs_axes[motor]}' "
|
|
103
|
-
f"but must be assignmed to '{CS_LETTERS}"
|
|
104
|
-
) from err
|
|
105
|
-
if len(set(motor_cs_index.values())) != len(motor_cs_index.items()):
|
|
106
|
-
raise RuntimeError(
|
|
107
|
-
"Failed to fetch distinct CS Axes."
|
|
108
|
-
"Motors passed are assigned to the same CS Axis"
|
|
109
|
-
f"{list(motor_cs_index)}"
|
|
110
|
-
)
|
|
111
|
-
|
|
146
|
+
# Get the velocities and acceleration rates for each motor
|
|
147
|
+
max_velocity, acceleration_time = await asyncio.gather(
|
|
148
|
+
gather_dict({motor: motor.max_velocity.get_value() for motor in motors}),
|
|
149
|
+
gather_dict(
|
|
150
|
+
{motor: motor.acceleration_time.get_value() for motor in motors}
|
|
151
|
+
),
|
|
152
|
+
)
|
|
112
153
|
motor_acceleration_rate = {
|
|
113
|
-
motor:
|
|
114
|
-
for motor in velocities
|
|
154
|
+
motor: max_velocity[motor] / acceleration_time[motor] for motor in motors
|
|
115
155
|
}
|
|
116
|
-
|
|
117
156
|
return _PmacMotorInfo(
|
|
118
|
-
cs_port,
|
|
157
|
+
cs_port=cs_ports_set.pop(),
|
|
158
|
+
cs_number=cs_numbers_set.pop(),
|
|
159
|
+
motor_cs_index=motor_cs_index,
|
|
160
|
+
motor_acceleration_rate=motor_acceleration_rate,
|
|
161
|
+
motor_max_velocity=max_velocity,
|
|
119
162
|
)
|
|
120
163
|
|
|
121
164
|
|
|
@@ -8,7 +8,6 @@ from ._signal import (
|
|
|
8
8
|
tango_signal_w,
|
|
9
9
|
tango_signal_x,
|
|
10
10
|
)
|
|
11
|
-
from ._tango_readable import TangoReadable
|
|
12
11
|
from ._tango_transport import (
|
|
13
12
|
AttributeProxy,
|
|
14
13
|
CommandProxy,
|
|
@@ -50,7 +49,6 @@ __all__ = [
|
|
|
50
49
|
"tango_signal_w",
|
|
51
50
|
"tango_signal_x",
|
|
52
51
|
"TangoDevice",
|
|
53
|
-
"TangoReadable",
|
|
54
52
|
"TangoPolling",
|
|
55
53
|
"TangoDeviceConnector",
|
|
56
54
|
"TangoLongStringTable",
|
|
@@ -29,7 +29,7 @@ class TangoDevice(Device):
|
|
|
29
29
|
|
|
30
30
|
def __init__(
|
|
31
31
|
self,
|
|
32
|
-
trl: str
|
|
32
|
+
trl: str = "",
|
|
33
33
|
support_events: bool = False,
|
|
34
34
|
name: str = "",
|
|
35
35
|
auto_fill_signals: bool = True,
|
|
@@ -80,6 +80,9 @@ class TangoDeviceConnector(DeviceConnector):
|
|
|
80
80
|
self._support_events = support_events
|
|
81
81
|
self._auto_fill_signals = auto_fill_signals
|
|
82
82
|
|
|
83
|
+
def set_trl(self, trl: str):
|
|
84
|
+
self.trl = trl
|
|
85
|
+
|
|
83
86
|
def create_children_from_annotations(self, device: Device):
|
|
84
87
|
if not hasattr(self, "filler"):
|
|
85
88
|
self.filler = DeviceFiller(
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
from typing import Annotated as A
|
|
2
2
|
|
|
3
|
-
from ophyd_async.core import
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
DEFAULT_TIMEOUT,
|
|
5
|
+
AsyncStatus,
|
|
6
|
+
SignalR,
|
|
7
|
+
SignalRW,
|
|
8
|
+
SignalX,
|
|
9
|
+
StandardReadable,
|
|
10
|
+
)
|
|
4
11
|
from ophyd_async.core import StandardReadableFormat as Format
|
|
5
|
-
from ophyd_async.tango.core import
|
|
12
|
+
from ophyd_async.tango.core import TangoDevice, TangoPolling
|
|
6
13
|
|
|
7
14
|
|
|
8
|
-
class TangoCounter(
|
|
15
|
+
class TangoCounter(TangoDevice, StandardReadable):
|
|
9
16
|
"""Tango counting device."""
|
|
10
17
|
|
|
11
18
|
# Enter the name and type of the signals you want to use
|
ophyd_async/tango/demo/_mover.py
CHANGED
|
@@ -11,16 +11,17 @@ from ophyd_async.core import (
|
|
|
11
11
|
SignalR,
|
|
12
12
|
SignalRW,
|
|
13
13
|
SignalX,
|
|
14
|
+
StandardReadable,
|
|
14
15
|
WatchableAsyncStatus,
|
|
15
16
|
WatcherUpdate,
|
|
16
17
|
observe_value,
|
|
17
18
|
wait_for_value,
|
|
18
19
|
)
|
|
19
20
|
from ophyd_async.core import StandardReadableFormat as Format
|
|
20
|
-
from ophyd_async.tango.core import DevStateEnum,
|
|
21
|
+
from ophyd_async.tango.core import DevStateEnum, TangoDevice, TangoPolling
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
class TangoMover(
|
|
24
|
+
class TangoMover(TangoDevice, StandardReadable, Movable, Stoppable):
|
|
24
25
|
"""Tango moving device."""
|
|
25
26
|
|
|
26
27
|
# Enter the name and type of the signals you want to use
|
|
@@ -32,7 +33,7 @@ class TangoMover(TangoReadable, Movable, Stoppable):
|
|
|
32
33
|
# If a tango name clashes with a bluesky verb, add a trailing underscore
|
|
33
34
|
stop_: SignalX
|
|
34
35
|
|
|
35
|
-
def __init__(self, trl: str
|
|
36
|
+
def __init__(self, trl: str = "", name=""):
|
|
36
37
|
super().__init__(trl, name=name)
|
|
37
38
|
self.add_readables([self.position], Format.HINTED_SIGNAL)
|
|
38
39
|
self.add_readables([self.velocity], Format.CONFIG_SIGNAL)
|
ophyd_async/testing/__init__.py
CHANGED
|
@@ -13,15 +13,6 @@ from ._assert import (
|
|
|
13
13
|
assert_value,
|
|
14
14
|
partial_reading,
|
|
15
15
|
)
|
|
16
|
-
from ._mock_signal_utils import (
|
|
17
|
-
callback_on_mock_put,
|
|
18
|
-
get_mock,
|
|
19
|
-
get_mock_put,
|
|
20
|
-
mock_puts_blocked,
|
|
21
|
-
set_mock_put_proceeds,
|
|
22
|
-
set_mock_value,
|
|
23
|
-
set_mock_values,
|
|
24
|
-
)
|
|
25
16
|
from ._one_of_everything import (
|
|
26
17
|
ExampleEnum,
|
|
27
18
|
ExampleTable,
|
|
@@ -38,6 +29,33 @@ from ._single_derived import (
|
|
|
38
29
|
)
|
|
39
30
|
from ._wait_for_pending import wait_for_pending_wakeups
|
|
40
31
|
|
|
32
|
+
|
|
33
|
+
# Back compat - delete before 1.0
|
|
34
|
+
def __getattr__(name):
|
|
35
|
+
import warnings
|
|
36
|
+
|
|
37
|
+
import ophyd_async.core
|
|
38
|
+
|
|
39
|
+
moved_to_core = {
|
|
40
|
+
"callback_on_mock_put",
|
|
41
|
+
"get_mock",
|
|
42
|
+
"get_mock_put",
|
|
43
|
+
"mock_puts_blocked",
|
|
44
|
+
"set_mock_put_proceeds",
|
|
45
|
+
"set_mock_value",
|
|
46
|
+
"set_mock_values",
|
|
47
|
+
}
|
|
48
|
+
if name in moved_to_core:
|
|
49
|
+
warnings.warn(
|
|
50
|
+
DeprecationWarning(
|
|
51
|
+
f"ophyd_async.testing.{name} has moved to ophyd_async.core"
|
|
52
|
+
),
|
|
53
|
+
stacklevel=2,
|
|
54
|
+
)
|
|
55
|
+
return getattr(ophyd_async.core, name)
|
|
56
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
57
|
+
|
|
58
|
+
|
|
41
59
|
# The order of this list determines the order of the documentation,
|
|
42
60
|
# so does not match the alphabetical order of the imports
|
|
43
61
|
__all__ = [
|
|
@@ -49,14 +67,6 @@ __all__ = [
|
|
|
49
67
|
"assert_describe_signal",
|
|
50
68
|
"assert_emitted",
|
|
51
69
|
"partial_reading",
|
|
52
|
-
# Mocking utilities
|
|
53
|
-
"get_mock",
|
|
54
|
-
"set_mock_value",
|
|
55
|
-
"set_mock_values",
|
|
56
|
-
"get_mock_put",
|
|
57
|
-
"callback_on_mock_put",
|
|
58
|
-
"mock_puts_blocked",
|
|
59
|
-
"set_mock_put_proceeds",
|
|
60
70
|
# Wait for pending wakeups
|
|
61
71
|
"wait_for_pending_wakeups",
|
|
62
72
|
"ExampleEnum",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ophyd-async
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango
|
|
5
5
|
Author-email: Tom Cobb <tom.cobb@diamond.ac.uk>
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
ophyd_async/__init__.py,sha256=dcAA3qsj1nNIMe5l-v2tlduZ_ypwBmyuHe45Lsq4k4w,206
|
|
2
2
|
ophyd_async/__main__.py,sha256=n_U4O9bgm97OuboUB_9eK7eFiwy8BZSgXJ0OzbE0DqU,481
|
|
3
3
|
ophyd_async/_docs_parser.py,sha256=gPYrigfSbYCF7QoSf2UvE-cpQu4snSssl7ZWN-kKDzI,352
|
|
4
|
-
ophyd_async/_version.py,sha256=
|
|
4
|
+
ophyd_async/_version.py,sha256=Byw420VruQzJ1exmj6PcZ9zpcSTgfBRBp5ZU4O6rwSc,706
|
|
5
5
|
ophyd_async/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
ophyd_async/core/__init__.py,sha256=
|
|
6
|
+
ophyd_async/core/__init__.py,sha256=3YD4rtXXJy5HWBsefK0nC-j2xC5tfiFbD7KJbN3nFvw,6008
|
|
7
7
|
ophyd_async/core/_derived_signal.py,sha256=TuZza_j3J1Bw4QSqBYB9Ta2FyQP5BycO3nSHVtJ890Q,13015
|
|
8
8
|
ophyd_async/core/_derived_signal_backend.py,sha256=Ibce9JHghiI5Ir8w0pUYULHL2qWkobeUYc0-CDrsO2E,12615
|
|
9
9
|
ophyd_async/core/_detector.py,sha256=9fYbBPmRnMGADcDTYkspDAL2uzhtNNiKCEeBUU0oKaY,14942
|
|
10
|
-
ophyd_async/core/_device.py,sha256=
|
|
10
|
+
ophyd_async/core/_device.py,sha256=tm-khZLMy-Q7nn86GYHkbXRJOc83DgkbrdQ6WCjZhUs,17638
|
|
11
11
|
ophyd_async/core/_device_filler.py,sha256=MDz8eQQ-eEAwo-UEMxfqPfpcBuMG01tLCGR6utwVnmE,14825
|
|
12
12
|
ophyd_async/core/_enums.py,sha256=2vh6x0rZ6SLiw2xxq1xVIn-GpbLDFc8wZoVdA55QiE8,370
|
|
13
13
|
ophyd_async/core/_flyer.py,sha256=8zKyU5aQOr_t59GIUwsYeb8NSabdvBp0swwuRe4v5VQ,3457
|
|
14
14
|
ophyd_async/core/_hdf_dataset.py,sha256=0bIX_ZbFSMdXqDwRtEvV-0avHnwXhjPddE5GVNmo7H8,2608
|
|
15
15
|
ophyd_async/core/_log.py,sha256=DxKR4Nz3SgTaTzKBZWqt-w48yT8WUAr_3Qr223TEWRw,3587
|
|
16
|
-
ophyd_async/core/_mock_signal_backend.py,sha256=
|
|
16
|
+
ophyd_async/core/_mock_signal_backend.py,sha256=cFovZEwwqYKV2LuQDZStSr3TFFvA31IJeSRoheHnZ8Q,3401
|
|
17
|
+
ophyd_async/core/_mock_signal_utils.py,sha256=ePFBDaon2lFT0vcnAaCRuoLr953QV6tYgHmY2fXQW-8,5520
|
|
17
18
|
ophyd_async/core/_protocol.py,sha256=wQ_snxhTprHqEjQb1HgFwBljwolMY6A8C3xgV1PXwdU,4051
|
|
18
19
|
ophyd_async/core/_providers.py,sha256=WBht3QCgvGc0stNcwH6z4Zr6hAz3e01-88NjsYI2w6I,9740
|
|
19
20
|
ophyd_async/core/_readable.py,sha256=iBo1YwA5bsAbzLbznvmSnzKDWUuGkLh850Br3BXsgeU,11707
|
|
20
21
|
ophyd_async/core/_settings.py,sha256=_ZccbXKP7j5rG6-bMKk7aaLr8hChdRDAPY_YSR71XXM,4213
|
|
21
|
-
ophyd_async/core/_signal.py,sha256=
|
|
22
|
+
ophyd_async/core/_signal.py,sha256=OEfjW_BIC3dVxgsWC0aVUj9rQL36KHkNTBCUVXjSRFo,28274
|
|
22
23
|
ophyd_async/core/_signal_backend.py,sha256=F3ma45cIIJ3D702zsVZIqn4Jv7u05YzMQBQND70QCbQ,6987
|
|
23
24
|
ophyd_async/core/_soft_signal_backend.py,sha256=NJUuyaCKtBZjggt8WKi7_lKQRHasToxviuQvl5xbhLU,6222
|
|
24
25
|
ophyd_async/core/_status.py,sha256=a2IDvv_GvUcFuhjQA5bQzWm9ngR6zGc9PR4XcZiaeqk,6557
|
|
25
26
|
ophyd_async/core/_table.py,sha256=ryJ7AwJBglQUzwP9_aSjR8cu8EKvYXfo1q1byhke3Uc,7248
|
|
26
|
-
ophyd_async/core/_utils.py,sha256=
|
|
27
|
+
ophyd_async/core/_utils.py,sha256=xv03NYanpeKEsErZkpkkGffwdSs7UElHtJQZ--1KLz8,11207
|
|
27
28
|
ophyd_async/core/_yaml_settings.py,sha256=Qojhku9l5kPSkTnEylCRWTe0gpw6S_XP5av5dPpqFgQ,2089
|
|
28
29
|
ophyd_async/epics/__init__.py,sha256=ou4yEaH9VZHz70e8oM614-arLMQvUfQyXhRJsnEpWn8,60
|
|
29
|
-
ophyd_async/epics/motor.py,sha256=
|
|
30
|
+
ophyd_async/epics/motor.py,sha256=ZFr6n3IE3qW68sRwNEj_8OnJOQebmkswCMK9qXP0ZqA,11803
|
|
30
31
|
ophyd_async/epics/signal.py,sha256=0A-supp9ajr63O6aD7F9oG0-Q26YmRjk-ZGh57-jo1Y,239
|
|
31
32
|
ophyd_async/epics/adandor/__init__.py,sha256=dlitllrAdhvh16PAcVMUSSEytTDNMu6_HuYk8KD1EoY,343
|
|
32
33
|
ophyd_async/epics/adandor/_andor.py,sha256=TijGjNVxuH-P0X7UACPt9eLLQ449DwMyVhbn1kV7Le8,1245
|
|
@@ -83,10 +84,10 @@ ophyd_async/epics/demo/point_detector_channel.db,sha256=FZ9H6HjqplhcF2jgimv_dT1n
|
|
|
83
84
|
ophyd_async/epics/odin/__init__.py,sha256=7kRqVzwoD8PVtp7Nj9iQWlgbLeoWE_8oiq-B0kixwTE,93
|
|
84
85
|
ophyd_async/epics/odin/_odin_io.py,sha256=YDBrS15PnEKe5SHmz397Emh--lZSQEnbR3G7p8pbShY,6533
|
|
85
86
|
ophyd_async/epics/pmac/__init__.py,sha256=GqJTiJudqE9pu050ZNED09F9tKRfazn0wBsojsMH2gg,273
|
|
86
|
-
ophyd_async/epics/pmac/_pmac_io.py,sha256=
|
|
87
|
-
ophyd_async/epics/pmac/_pmac_trajectory.py,sha256=
|
|
87
|
+
ophyd_async/epics/pmac/_pmac_io.py,sha256=cbChieNrDWRzrr5Mdsqtm2Azp8sG0KHP9rGeJxmbYrA,4332
|
|
88
|
+
ophyd_async/epics/pmac/_pmac_trajectory.py,sha256=hzAcpLNmFoNceubsjk6mjsmg6-PgSjWUu1a8Exyvi6I,7729
|
|
88
89
|
ophyd_async/epics/pmac/_pmac_trajectory_generation.py,sha256=3IIxXa0r6-2uNnILKLGxp3xosOZx8MubKF-F_OM7uaw,27331
|
|
89
|
-
ophyd_async/epics/pmac/_utils.py,sha256=
|
|
90
|
+
ophyd_async/epics/pmac/_utils.py,sha256=MfuY6NicT7wkwVIWAZkWoCu1ZoSzy6jda1wLK9XAOLA,8614
|
|
90
91
|
ophyd_async/epics/testing/__init__.py,sha256=aTIv4D2DYrpnGco5RQF8QuLG1SfFkIlTyM2uYEKXltA,522
|
|
91
92
|
ophyd_async/epics/testing/_example_ioc.py,sha256=zb4ZEUzuB2MrSw5ETPLIiHhf-2BRU1Bdxco6Kh4iI1I,3880
|
|
92
93
|
ophyd_async/epics/testing/_utils.py,sha256=9gxpwaWX0HGtacu1LTupcw7viXN8G78RmuNciU_-cjs,1702
|
|
@@ -131,32 +132,30 @@ ophyd_async/sim/_pattern_generator.py,sha256=kuxvyX2gIxrywhQRhaO1g8YluBT7LBkE20I
|
|
|
131
132
|
ophyd_async/sim/_point_detector.py,sha256=wMG_ncvm99WMCPihlFyuMEf3UknAxCpB1hpk3uKiENE,3024
|
|
132
133
|
ophyd_async/sim/_stage.py,sha256=_SywbmSQwxf7JLx68qwo0RpiB3oIWlbTLmvRKxUoig0,1602
|
|
133
134
|
ophyd_async/tango/__init__.py,sha256=g9xzjlzPpUAP12YI-kYwfAoLSYPAQdL1S11R2c-cius,60
|
|
134
|
-
ophyd_async/tango/core/__init__.py,sha256=
|
|
135
|
-
ophyd_async/tango/core/_base_device.py,sha256=
|
|
135
|
+
ophyd_async/tango/core/__init__.py,sha256=dO2tG_y61zZFQRQh5L37Ps-IqNf-DGOT77Ov5Kobfhs,1349
|
|
136
|
+
ophyd_async/tango/core/_base_device.py,sha256=X5ncxaWKOfRhhqPyT8tmTBJGc3ldGthw1ZCe_j_M2Tg,5088
|
|
136
137
|
ophyd_async/tango/core/_converters.py,sha256=xI_RhMR8dY6IVORUZVVCL9LdYnEE6TA6BBPX_lTu06w,2183
|
|
137
138
|
ophyd_async/tango/core/_signal.py,sha256=8mIxRVEVjhDN33LDbbKZWGMUYn9Gl5ZMEIYw6GSBTUE,5569
|
|
138
|
-
ophyd_async/tango/core/_tango_readable.py,sha256=ctR6YcBGGatW6Jp2kvddA1hVZ2v1CidPsF9FmJK9BYg,406
|
|
139
139
|
ophyd_async/tango/core/_tango_transport.py,sha256=KxjhHqKADrOvzGi9tbOQXUWdsJ0NKGejWxHItxpUsjg,37401
|
|
140
140
|
ophyd_async/tango/core/_utils.py,sha256=pwT7V1DNWSyPOSzvDZ6OsDZTjaV-pAeDLDlmgtHVcNM,1673
|
|
141
141
|
ophyd_async/tango/demo/__init__.py,sha256=_j-UicTnckuIBp8PnieFMOMnLFGivnaKdmo9o0hYtzc,256
|
|
142
|
-
ophyd_async/tango/demo/_counter.py,sha256=
|
|
142
|
+
ophyd_async/tango/demo/_counter.py,sha256=m6zxOJLbHgCEBAapVc1UiOOqKj5lvrlxjA6mXWMRMjo,1200
|
|
143
143
|
ophyd_async/tango/demo/_detector.py,sha256=X5YWHAjukKZ7iYF1fBNle4CBDj1X5rvj0lnPMOcnRCU,1340
|
|
144
|
-
ophyd_async/tango/demo/_mover.py,sha256=
|
|
144
|
+
ophyd_async/tango/demo/_mover.py,sha256=FyG9g1TLaWoqjbLblqWK8inMuDcNVlioq0MIeD5npz4,2913
|
|
145
145
|
ophyd_async/tango/demo/_tango/__init__.py,sha256=FfONT7vM49nNo3a1Lv-LcMZO9EHv6bv91yY-RnxIib4,85
|
|
146
146
|
ophyd_async/tango/demo/_tango/_servers.py,sha256=putvERDyibibaTbhdWyqZB_axj2fURXqzDsZb9oSW14,2991
|
|
147
147
|
ophyd_async/tango/testing/__init__.py,sha256=l52SmX9XuxZUBuLpOYJzHfskkWVYhx3RkSbGL_wUu5Y,199
|
|
148
148
|
ophyd_async/tango/testing/_one_of_everything.py,sha256=eJg5K8n1ExwPfruDCHNZcWjx4aRTA1Vs_7NQHSHjpgc,6851
|
|
149
149
|
ophyd_async/tango/testing/_test_config.py,sha256=i3t5d4wjUEtAvvSSZNz_bH_r5VEvUphUcEOEd8LKxQQ,228
|
|
150
|
-
ophyd_async/testing/__init__.py,sha256=
|
|
150
|
+
ophyd_async/testing/__init__.py,sha256=0x4kehIkNoR_H-gzj0yJ-SmtJxxSBnyeOcxYcNqvgkY,2033
|
|
151
151
|
ophyd_async/testing/__pytest_assert_rewrite.py,sha256=_SU2UfChPgEf7CFY7aYH2B7MLp-07_qYnVLyu6QtDL8,129
|
|
152
152
|
ophyd_async/testing/_assert.py,sha256=Ss_XDToi1ymUfr0Z1r45A2Fmg7-9UOv9gYkJEBsZPv8,8795
|
|
153
|
-
ophyd_async/testing/_mock_signal_utils.py,sha256=GOjELaRFg9zJKcpeLFXjN7ViMT1AK2Hu52lkLMI5XUc,5393
|
|
154
153
|
ophyd_async/testing/_one_of_everything.py,sha256=U9ui7B-iNHDM3H3hIWUuaCb8Gc2eLlUh0sBHUlQldT0,4741
|
|
155
154
|
ophyd_async/testing/_single_derived.py,sha256=5-HOTzgePcZ354NK_ssVpyIbJoJmKyjVQCxSwQXUC-4,2730
|
|
156
155
|
ophyd_async/testing/_utils.py,sha256=zClRo5ve8RGia7wQnby41W-Zprj-slOA5da1LfYnuhw,45
|
|
157
156
|
ophyd_async/testing/_wait_for_pending.py,sha256=YZAR48n-CW0GsPey3zFRzMJ4byDAr3HvMIoawjmTrHw,732
|
|
158
|
-
ophyd_async-0.
|
|
159
|
-
ophyd_async-0.
|
|
160
|
-
ophyd_async-0.
|
|
161
|
-
ophyd_async-0.
|
|
162
|
-
ophyd_async-0.
|
|
157
|
+
ophyd_async-0.14.0.dist-info/licenses/LICENSE,sha256=pU5shZcsvWgz701EbT7yjFZ8rMvZcWgRH54CRt8ld_c,1517
|
|
158
|
+
ophyd_async-0.14.0.dist-info/METADATA,sha256=1H2TzMmCIJfib07o1qSOHNYJVDHgqkwIUn9wPjzoYNU,5703
|
|
159
|
+
ophyd_async-0.14.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
160
|
+
ophyd_async-0.14.0.dist-info/top_level.txt,sha256=-hjorMsv5Rmjo3qrgqhjpal1N6kW5vMxZO3lD4iEaXs,12
|
|
161
|
+
ophyd_async-0.14.0.dist-info/RECORD,,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from ophyd_async.core import StandardReadable
|
|
4
|
-
|
|
5
|
-
from ._base_device import TangoDevice
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class TangoReadable(TangoDevice, StandardReadable):
|
|
9
|
-
def __init__(
|
|
10
|
-
self,
|
|
11
|
-
trl: str | None = None,
|
|
12
|
-
name: str = "",
|
|
13
|
-
auto_fill_signals: bool = True,
|
|
14
|
-
) -> None:
|
|
15
|
-
TangoDevice.__init__(self, trl, name=name, auto_fill_signals=auto_fill_signals)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|