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 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.13.6'
32
- __version_tuple__ = version_tuple = (0, 13, 6)
31
+ __version__ = version = '0.14.0'
32
+ __version_tuple__ = version_tuple = (0, 14, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -14,7 +14,15 @@ from ._detector import (
14
14
  StandardDetector,
15
15
  TriggerInfo,
16
16
  )
17
- from ._device import Device, DeviceConnector, DeviceVector, init_devices
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",
@@ -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: LazyMock):
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
- await child_device.connect(mock=mock.child(name))
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: LazyMock | None = None
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 | LazyMock = False,
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
- [](#LazyMock) then pass this down for use within the Signals,
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, LazyMock):
192
- # Use the provided mock
250
+ if isinstance(mock, DeviceMock):
251
+ # Use the user supplied mock
193
252
  self._mock = mock
194
253
  elif not self._mock:
195
- # Make one
196
- self._mock = LazyMock()
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, LazyMock
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 ophyd_async.core import (
6
- Device,
7
- LazyMock,
8
- MockSignalBackend,
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
- assert isinstance(mock, LazyMock), f"Device {device} not connected in mock mode"
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
- assert isinstance(connector, SignalConnector), f"Expected Signal, got {signal}"
29
- assert isinstance(connector.backend, MockSignalBackend), (
30
- f"Signal {signal} not connected in mock mode"
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
 
@@ -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" {matcher.__name__}() in {timeout}s"
644
+ f" {matcher_name}() in {timeout}s"
645
645
  ) from exc
646
646
 
647
647
  return status
@@ -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
 
@@ -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
- motor_lower_limit, motor_upper_limit, egu = await asyncio.gather(
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
- if motor_lower_limit == 0 and motor_upper_limit == 0:
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
- CS_LETTERS = "ABCUVWXYZ"
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 + 1: epics_signal_rw(
24
- Array1D[np.float64], f"{prefix}{letter}:Positions"
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 + 1: epics_signal_rw(bool, f"{prefix}{letter}:UseAxis")
32
- for i, letter in enumerate(CS_LETTERS)
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 + 1: epics_signal_rw(
38
- Array1D[np.float64], f"{prefix}{letter}:Velocities"
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 + 1: epics_signal_rw(float, f"{prefix}M{i + 1}:DirectDemand")
80
- for i in range(len(CS_LETTERS))
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
- from ophyd_async.epics.pmac import PmacIO
19
- from ophyd_async.epics.pmac._pmac_io import CS_LETTERS
20
- from ophyd_async.epics.pmac._pmac_trajectory_generation import PVT, Trajectory
21
- from ophyd_async.epics.pmac._utils import (
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
- axis + 1: (axis in motor_info.motor_cs_index.values())
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, number in motor_info.motor_cs_index.items():
179
+ for motor, cs_index in motor_info.motor_cs_index.items():
181
180
  coros.append(
182
- self.pmac.trajectory.positions[number + 1].set(
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[number + 1].set(
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] + 1],
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,
@@ -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 CS_LETTERS, PmacIO
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
- assignments = {
48
- motor: pmac.assignment[pmac.motor_assignment_index[motor]]
49
- for motor in motors
50
- }
51
-
52
- cs_ports, cs_numbers, cs_axes, velocities, accls = await asyncio.gather(
53
- gather_dict(
54
- {motor: assignments[motor].cs_port.get_value() for motor in motors}
55
- ),
56
- gather_dict(
57
- {motor: assignments[motor].cs_number.get_value() for motor in motors}
58
- ),
59
- gather_dict(
60
- {
61
- motor: assignments[motor].cs_axis_letter.get_value()
62
- for motor in motors
63
- }
64
- ),
65
- gather_dict({motor: motor.max_velocity.get_value() for motor in motors}),
66
- gather_dict(
67
- {motor: motor.acceleration_time.get_value() for motor in motors}
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
- cs_ports = set(cs_ports.values())
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(cs_ports)}"
136
+ f"{list(cs_ports_set)}"
79
137
  )
80
-
81
- cs_port = cs_ports.pop()
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(cs_numbers)}"
143
+ f"{list(cs_numbers_set)}"
89
144
  )
90
145
 
91
- cs_number = cs_numbers.pop()
92
-
93
- motor_cs_index = {}
94
- for motor in cs_axes:
95
- try:
96
- if not cs_axes[motor]:
97
- raise ValueError
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: float(velocities[motor]) / float(accls[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, cs_number, motor_cs_index, motor_acceleration_rate, velocities
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 | None,
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 DEFAULT_TIMEOUT, AsyncStatus, SignalR, SignalRW, SignalX
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 TangoPolling, TangoReadable
12
+ from ophyd_async.tango.core import TangoDevice, TangoPolling
6
13
 
7
14
 
8
- class TangoCounter(TangoReadable):
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
@@ -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, TangoPolling, TangoReadable
21
+ from ophyd_async.tango.core import DevStateEnum, TangoDevice, TangoPolling
21
22
 
22
23
 
23
- class TangoMover(TangoReadable, Movable, Stoppable):
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 | None = "", name=""):
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)
@@ -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.13.6
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=opPR5vtgMqtAtI-1B5Bg_MEreSc0tb9JwaQhHrD7xNE,706
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=yGbkVju5otO8DmA3KyerYB0gLtcX5sdxlq3ajyGha4M,5531
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=_M3hwiYv8xO-7pfQkOzeZEEsB05Oa2HTRKOEc69C_a8,14743
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=N6rzwBR3QwHZMcifIZg6sbno6-Ts-1jK0aFVt-3jHAg,3289
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=085vcyjhEyZECi4Svjq6DUM5kxrzZh8s5DoHR2LALOc,28195
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=gUewO4XPrBxsEWzObNumqAB2Q7Hwrd5F_Nc6B2XwQIM,12532
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=nS6Vx4-6vYso3b_QtpE8p8dlys5jRr2LK7prZCkZMc8,9558
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=E7tdaq9FAM6BeGQG1L8ALEYpXOlyqTXl_NLLSWzmCdk,4345
87
- ophyd_async/epics/pmac/_pmac_trajectory.py,sha256=7wTaetNNy9uj7C_skcs0VH5BthuY4Ec5zn-m4ROxd8k,7866
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=n4vh9n7hmaWe9g02FtguF2oDsYuVvsTgmK7fYEyGuIE,6092
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=OOVdHu07cssK90F-caG0CY7qKpPYy0MSV421YNAI-_8,1413
135
- ophyd_async/tango/core/_base_device.py,sha256=e9oqSL-fDOj8r9nUUFZkbibhRGbI6HYtlnZjK5B_2fE,5033
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=2J4SCHnBWLF0O5mFWlJdO4tmnElvlx5sRrk4op_AC9U,1139
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=i-Tq5nDmYi4RcC4O6mOJoVeMEIIxuqyS_2AfjTpAcnk,2884
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=jDBzUAHGDMfkhd-_9u0CJWEq0E0sPrIGGlLmVzEyxY8,1742
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.13.6.dist-info/licenses/LICENSE,sha256=pU5shZcsvWgz701EbT7yjFZ8rMvZcWgRH54CRt8ld_c,1517
159
- ophyd_async-0.13.6.dist-info/METADATA,sha256=o3yNVUgIIR1UiWECtmQX9WZLuZAu4et3sru1DKoOsDM,5703
160
- ophyd_async-0.13.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
161
- ophyd_async-0.13.6.dist-info/top_level.txt,sha256=-hjorMsv5Rmjo3qrgqhjpal1N6kW5vMxZO3lD4iEaXs,12
162
- ophyd_async-0.13.6.dist-info/RECORD,,
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)