ophyd-async 0.13.3__py3-none-any.whl → 0.13.5__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 +26 -3
- ophyd_async/core/_derived_signal_backend.py +2 -1
- ophyd_async/core/_detector.py +2 -2
- ophyd_async/core/_device.py +9 -9
- ophyd_async/core/_enums.py +5 -0
- ophyd_async/core/_signal.py +34 -38
- ophyd_async/core/_signal_backend.py +3 -1
- ophyd_async/core/_status.py +2 -2
- ophyd_async/core/_table.py +8 -0
- ophyd_async/core/_utils.py +11 -11
- ophyd_async/epics/adcore/_core_logic.py +3 -1
- ophyd_async/epics/adcore/_utils.py +4 -4
- ophyd_async/epics/core/_aioca.py +2 -2
- ophyd_async/epics/core/_p4p.py +2 -2
- ophyd_async/epics/motor.py +28 -7
- ophyd_async/epics/pmac/_pmac_io.py +8 -4
- ophyd_async/epics/pmac/_pmac_trajectory.py +144 -41
- ophyd_async/epics/pmac/_pmac_trajectory_generation.py +692 -0
- ophyd_async/epics/pmac/_utils.py +1 -681
- ophyd_async/fastcs/jungfrau/__init__.py +2 -1
- ophyd_async/fastcs/jungfrau/_controller.py +29 -11
- ophyd_async/fastcs/jungfrau/_utils.py +10 -2
- ophyd_async/fastcs/panda/__init__.py +10 -0
- ophyd_async/fastcs/panda/_block.py +14 -0
- ophyd_async/fastcs/panda/_trigger.py +123 -3
- ophyd_async/sim/_motor.py +4 -2
- ophyd_async/sim/_stage.py +14 -4
- ophyd_async/tango/core/__init__.py +17 -3
- ophyd_async/tango/core/_signal.py +18 -22
- ophyd_async/tango/core/_tango_transport.py +407 -239
- ophyd_async/tango/core/_utils.py +9 -0
- ophyd_async/tango/demo/_mover.py +1 -2
- ophyd_async/tango/testing/__init__.py +2 -1
- ophyd_async/tango/testing/_one_of_everything.py +13 -5
- ophyd_async/tango/testing/_test_config.py +11 -0
- ophyd_async/testing/_assert.py +2 -2
- {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/METADATA +2 -36
- {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/RECORD +42 -40
- {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/WHEEL +0 -0
- {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/licenses/LICENSE +0 -0
- {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.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.13.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 13,
|
|
31
|
+
__version__ = version = '0.13.5'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 13, 5)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
ophyd_async/core/__init__.py
CHANGED
|
@@ -21,6 +21,7 @@ from ._enums import (
|
|
|
21
21
|
EnableDisable,
|
|
22
22
|
InOut,
|
|
23
23
|
OnOff,
|
|
24
|
+
YesNo,
|
|
24
25
|
)
|
|
25
26
|
from ._flyer import FlyerController, FlyMotorInfo, StandardFlyer
|
|
26
27
|
from ._hdf_dataset import HDFDatasetDescription, HDFDocumentComposer
|
|
@@ -78,7 +79,7 @@ from ._signal_backend import (
|
|
|
78
79
|
)
|
|
79
80
|
from ._soft_signal_backend import SoftSignalBackend
|
|
80
81
|
from ._status import AsyncStatus, WatchableAsyncStatus, completed_status
|
|
81
|
-
from ._table import Table
|
|
82
|
+
from ._table import Table, TableSubclass
|
|
82
83
|
from ._utils import (
|
|
83
84
|
CALCULATE_TIMEOUT,
|
|
84
85
|
DEFAULT_TIMEOUT,
|
|
@@ -87,7 +88,7 @@ from ._utils import (
|
|
|
87
88
|
ConfinedModel,
|
|
88
89
|
EnumTypes,
|
|
89
90
|
LazyMock,
|
|
90
|
-
|
|
91
|
+
NotConnectedError,
|
|
91
92
|
Reference,
|
|
92
93
|
StrictEnum,
|
|
93
94
|
SubsetEnum,
|
|
@@ -103,6 +104,26 @@ from ._utils import (
|
|
|
103
104
|
)
|
|
104
105
|
from ._yaml_settings import YamlSettingsProvider
|
|
105
106
|
|
|
107
|
+
|
|
108
|
+
# Back compat - delete before 1.0
|
|
109
|
+
def __getattr__(name):
|
|
110
|
+
import warnings
|
|
111
|
+
|
|
112
|
+
renames = {
|
|
113
|
+
"NotConnected": NotConnectedError,
|
|
114
|
+
}
|
|
115
|
+
rename = renames.get(name)
|
|
116
|
+
if rename is not None:
|
|
117
|
+
warnings.warn(
|
|
118
|
+
DeprecationWarning(
|
|
119
|
+
f"{name!r} is deprecated, use {rename.__name__!r} instead"
|
|
120
|
+
),
|
|
121
|
+
stacklevel=2,
|
|
122
|
+
)
|
|
123
|
+
return rename
|
|
124
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
125
|
+
|
|
126
|
+
|
|
106
127
|
__all__ = [
|
|
107
128
|
# Device
|
|
108
129
|
"Device",
|
|
@@ -195,7 +216,7 @@ __all__ = [
|
|
|
195
216
|
"DEFAULT_TIMEOUT",
|
|
196
217
|
"Callback",
|
|
197
218
|
"ConfinedModel",
|
|
198
|
-
"
|
|
219
|
+
"NotConnectedError",
|
|
199
220
|
"Reference",
|
|
200
221
|
"error_if_none",
|
|
201
222
|
"gather_dict",
|
|
@@ -221,4 +242,6 @@ __all__ = [
|
|
|
221
242
|
"EnableDisable",
|
|
222
243
|
"InOut",
|
|
223
244
|
"OnOff",
|
|
245
|
+
"YesNo",
|
|
246
|
+
"TableSubclass",
|
|
224
247
|
]
|
|
@@ -222,7 +222,8 @@ class SignalTransformer(Generic[TransformT]):
|
|
|
222
222
|
# callback
|
|
223
223
|
self._cached_readings = {}
|
|
224
224
|
for raw in self.raw_and_transform_subscribables.values():
|
|
225
|
-
|
|
225
|
+
# can remove type: ignore when Subscribable protocol updated
|
|
226
|
+
raw.subscribe_reading(self._update_cached_reading) # type: ignore
|
|
226
227
|
elif self._complete_cached_reading():
|
|
227
228
|
# Callback on the last complete set of readings
|
|
228
229
|
derived_readings = self._make_derived_readings(self._cached_readings)
|
ophyd_async/core/_detector.py
CHANGED
|
@@ -249,11 +249,11 @@ class StandardDetector(
|
|
|
249
249
|
)
|
|
250
250
|
try:
|
|
251
251
|
await signal.get_value()
|
|
252
|
-
except NotImplementedError as
|
|
252
|
+
except NotImplementedError as exc:
|
|
253
253
|
raise Exception(
|
|
254
254
|
f"config signal {signal.name} must be connected before it is "
|
|
255
255
|
+ "passed to the detector"
|
|
256
|
-
) from
|
|
256
|
+
) from exc
|
|
257
257
|
|
|
258
258
|
@AsyncStatus.wrap
|
|
259
259
|
async def unstage(self) -> None:
|
ophyd_async/core/_device.py
CHANGED
|
@@ -13,7 +13,7 @@ from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
|
|
|
13
13
|
from ._utils import (
|
|
14
14
|
DEFAULT_TIMEOUT,
|
|
15
15
|
LazyMock,
|
|
16
|
-
|
|
16
|
+
NotConnectedError,
|
|
17
17
|
error_if_none,
|
|
18
18
|
wait_for_connection,
|
|
19
19
|
)
|
|
@@ -51,10 +51,10 @@ class DeviceConnector:
|
|
|
51
51
|
for name, child_device in device.children():
|
|
52
52
|
try:
|
|
53
53
|
await child_device.connect(mock=mock.child(name))
|
|
54
|
-
except Exception as
|
|
55
|
-
exceptions[name] =
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
exceptions[name] = exc
|
|
56
56
|
if exceptions:
|
|
57
|
-
raise
|
|
57
|
+
raise NotConnectedError.with_other_exceptions_logged(exceptions)
|
|
58
58
|
|
|
59
59
|
async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
|
|
60
60
|
"""Use during [](#Device.connect) with `mock=False`.
|
|
@@ -62,7 +62,7 @@ class DeviceConnector:
|
|
|
62
62
|
This is called when there is no cached connect done in `mock=False`
|
|
63
63
|
mode. It connects the Device and all its children in real mode in parallel.
|
|
64
64
|
"""
|
|
65
|
-
# Connect in parallel, gathering up
|
|
65
|
+
# Connect in parallel, gathering up NotConnectedErrors
|
|
66
66
|
coros = {
|
|
67
67
|
name: child_device.connect(timeout=timeout, force_reconnect=force_reconnect)
|
|
68
68
|
for name, child_device in device.children()
|
|
@@ -334,12 +334,12 @@ class DeviceProcessor:
|
|
|
334
334
|
self._locals_on_exit = self._caller_locals()
|
|
335
335
|
try:
|
|
336
336
|
fut = call_in_bluesky_event_loop(self._on_exit())
|
|
337
|
-
except RuntimeError as
|
|
338
|
-
raise
|
|
337
|
+
except RuntimeError as exc:
|
|
338
|
+
raise NotConnectedError(
|
|
339
339
|
"Could not connect devices. Is the bluesky event loop running? See "
|
|
340
340
|
"https://blueskyproject.io/ophyd-async/main/"
|
|
341
341
|
"user/explanations/event-loop-choice.html for more info."
|
|
342
|
-
) from
|
|
342
|
+
) from exc
|
|
343
343
|
return fut
|
|
344
344
|
|
|
345
345
|
async def _on_exit(self) -> None:
|
|
@@ -372,7 +372,7 @@ def init_devices(
|
|
|
372
372
|
:param mock: If True, connect Signals in mock mode.
|
|
373
373
|
:param timeout: How long to wait for connect before logging an exception.
|
|
374
374
|
:raises RuntimeError: If used inside a plan, use [](#ensure_connected) instead.
|
|
375
|
-
:raises
|
|
375
|
+
:raises NotConnectedError: If devices could not be connected.
|
|
376
376
|
|
|
377
377
|
For example, to connect and name 2 motors in parallel:
|
|
378
378
|
```python
|
ophyd_async/core/_enums.py
CHANGED
ophyd_async/core/_signal.py
CHANGED
|
@@ -39,8 +39,8 @@ from ._utils import (
|
|
|
39
39
|
async def _wait_for(coro: Awaitable[T], timeout: float | None, source: str) -> T:
|
|
40
40
|
try:
|
|
41
41
|
return await asyncio.wait_for(coro, timeout)
|
|
42
|
-
except TimeoutError as
|
|
43
|
-
raise TimeoutError(source) from
|
|
42
|
+
except TimeoutError as exc:
|
|
43
|
+
raise TimeoutError(source) from exc
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def _add_timeout(func):
|
|
@@ -117,17 +117,17 @@ class _SignalCache(Generic[SignalDatatypeT]):
|
|
|
117
117
|
def __init__(self, backend: SignalBackend[SignalDatatypeT], signal: Signal) -> None:
|
|
118
118
|
self._signal: Signal[Any] = signal
|
|
119
119
|
self._staged = False
|
|
120
|
-
self._listeners:
|
|
120
|
+
self._listeners: set[Callback] = set()
|
|
121
121
|
self._valid = asyncio.Event()
|
|
122
122
|
self._reading: Reading[SignalDatatypeT] | None = None
|
|
123
123
|
self.backend: SignalBackend[SignalDatatypeT] = backend
|
|
124
124
|
try:
|
|
125
125
|
asyncio.get_running_loop()
|
|
126
|
-
except RuntimeError as
|
|
126
|
+
except RuntimeError as exc:
|
|
127
127
|
raise RuntimeError(
|
|
128
128
|
"Need a running event loop to subscribe to a signal, "
|
|
129
129
|
"are you trying to run subscribe outside a plan?"
|
|
130
|
-
) from
|
|
130
|
+
) from exc
|
|
131
131
|
signal.log.debug(f"Making subscription on source {signal.source}")
|
|
132
132
|
backend.set_callback(self._callback)
|
|
133
133
|
|
|
@@ -154,29 +154,29 @@ class _SignalCache(Generic[SignalDatatypeT]):
|
|
|
154
154
|
)
|
|
155
155
|
self._reading = reading
|
|
156
156
|
self._valid.set()
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
# Copy the listeners in case one of the callbacks removes the listener
|
|
158
|
+
# from the set
|
|
159
|
+
for callback in list(self._listeners):
|
|
160
|
+
self._notify(callback)
|
|
160
161
|
|
|
161
162
|
def _notify(
|
|
162
163
|
self,
|
|
163
|
-
function: Callback[dict[str, Reading[SignalDatatypeT]]
|
|
164
|
-
want_value: bool,
|
|
164
|
+
function: Callback[dict[str, Reading[SignalDatatypeT]]],
|
|
165
165
|
) -> None:
|
|
166
|
-
function(self._ensure_reading()
|
|
167
|
-
{self._signal.name: self._ensure_reading()}
|
|
168
|
-
)
|
|
166
|
+
function({self._signal.name: self._ensure_reading()})
|
|
169
167
|
|
|
170
|
-
def subscribe(self, function: Callback
|
|
171
|
-
self._listeners
|
|
168
|
+
def subscribe(self, function: Callback) -> None:
|
|
169
|
+
self._listeners.add(function)
|
|
172
170
|
if self._valid.is_set():
|
|
173
|
-
self._notify(function
|
|
171
|
+
self._notify(function)
|
|
174
172
|
|
|
175
173
|
def unsubscribe(self, function: Callback) -> bool:
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
if function in self._listeners:
|
|
175
|
+
self._listeners.remove(function)
|
|
176
|
+
else:
|
|
178
177
|
self._signal.log.warning(
|
|
179
|
-
f"Unsubscribe failed
|
|
178
|
+
f"Unsubscribe failed for signal {self._signal.name}:"
|
|
179
|
+
f" subscriber {function} was not found"
|
|
180
180
|
f" in listeners list: {list(self._listeners)}"
|
|
181
181
|
)
|
|
182
182
|
return self._staged or bool(self._listeners)
|
|
@@ -244,24 +244,19 @@ class SignalR(Signal[SignalDatatypeT], AsyncReadable, AsyncStageable, Subscribab
|
|
|
244
244
|
self.log.debug(f"get_value() on source {self.source} returned {value}")
|
|
245
245
|
return value
|
|
246
246
|
|
|
247
|
-
def
|
|
248
|
-
"""Subscribe to updates in value of a device.
|
|
249
|
-
|
|
250
|
-
:param function: The callback function to call when the value changes.
|
|
251
|
-
"""
|
|
252
|
-
self._get_cache().subscribe(function, want_value=True)
|
|
253
|
-
|
|
254
|
-
def subscribe(
|
|
247
|
+
def subscribe_reading(
|
|
255
248
|
self, function: Callback[dict[str, Reading[SignalDatatypeT]]]
|
|
256
249
|
) -> None:
|
|
257
250
|
"""Subscribe to updates in the reading.
|
|
258
251
|
|
|
259
252
|
:param function: The callback function to call when the reading changes.
|
|
260
253
|
"""
|
|
261
|
-
self._get_cache().subscribe(function
|
|
254
|
+
self._get_cache().subscribe(function)
|
|
255
|
+
|
|
256
|
+
subscribe = subscribe_reading
|
|
262
257
|
|
|
263
258
|
def clear_sub(self, function: Callback) -> None:
|
|
264
|
-
"""Remove a subscription passed to `
|
|
259
|
+
"""Remove a subscription passed to `subscribe_reading`.
|
|
265
260
|
|
|
266
261
|
:param function: The callback function to remove.
|
|
267
262
|
"""
|
|
@@ -402,7 +397,7 @@ async def observe_value(
|
|
|
402
397
|
value being yielded, even if it is the same as the previous value.
|
|
403
398
|
|
|
404
399
|
:param signal:
|
|
405
|
-
Call
|
|
400
|
+
Call subscribe_reading on this at the start, and clear_sub on it at the end.
|
|
406
401
|
:param timeout:
|
|
407
402
|
If given, how long to wait for each updated value in seconds. If an
|
|
408
403
|
update is not produced in this time then raise asyncio.TimeoutError.
|
|
@@ -454,7 +449,7 @@ async def observe_signals_value(
|
|
|
454
449
|
value being yielded, even if it is the same as the previous value.
|
|
455
450
|
|
|
456
451
|
:param signals:
|
|
457
|
-
Call
|
|
452
|
+
Call subscribe_reading on all the signals at the start, and clear_sub on
|
|
458
453
|
it at the end.
|
|
459
454
|
:param timeout:
|
|
460
455
|
If given, how long to wait for ANY updated value from shared queue in seconds.
|
|
@@ -486,11 +481,12 @@ async def observe_signals_value(
|
|
|
486
481
|
# subscribe signal to update queue and fill cbs dict
|
|
487
482
|
for signal in signals:
|
|
488
483
|
|
|
489
|
-
def queue_value(
|
|
484
|
+
def queue_value(reading: dict[str, Reading[SignalDatatypeT]], signal=signal):
|
|
485
|
+
value = reading[signal.name]["value"]
|
|
490
486
|
q.put_nowait((signal, value))
|
|
491
487
|
|
|
492
488
|
cbs[signal] = queue_value
|
|
493
|
-
signal.
|
|
489
|
+
signal.subscribe_reading(queue_value)
|
|
494
490
|
|
|
495
491
|
if done_status is not None:
|
|
496
492
|
done_status.add_callback(q.put_nowait)
|
|
@@ -543,11 +539,11 @@ class _ValueChecker(Generic[SignalDatatypeT]):
|
|
|
543
539
|
):
|
|
544
540
|
try:
|
|
545
541
|
await asyncio.wait_for(self._wait_for_value(signal), timeout)
|
|
546
|
-
except TimeoutError as
|
|
542
|
+
except TimeoutError as exc:
|
|
547
543
|
raise TimeoutError(
|
|
548
544
|
f"{signal.name} didn't match {self._matcher_name} in {timeout}s, "
|
|
549
545
|
f"last value {self._last_value!r}"
|
|
550
|
-
) from
|
|
546
|
+
) from exc
|
|
551
547
|
|
|
552
548
|
|
|
553
549
|
async def wait_for_value(
|
|
@@ -558,7 +554,7 @@ async def wait_for_value(
|
|
|
558
554
|
"""Wait for a signal to have a matching value.
|
|
559
555
|
|
|
560
556
|
:param signal:
|
|
561
|
-
Call
|
|
557
|
+
Call subscribe_reading on this at the start, and clear_sub on it at the
|
|
562
558
|
end.
|
|
563
559
|
:param match:
|
|
564
560
|
If a callable, it should return True if the value matches. If not
|
|
@@ -642,11 +638,11 @@ async def set_and_wait_for_other_value(
|
|
|
642
638
|
await asyncio.wait_for(_wait_for_value(), timeout)
|
|
643
639
|
if wait_for_set_completion:
|
|
644
640
|
await status
|
|
645
|
-
except TimeoutError as
|
|
641
|
+
except TimeoutError as exc:
|
|
646
642
|
raise TimeoutError(
|
|
647
643
|
f"{match_signal.name} value didn't match value from"
|
|
648
644
|
f" {matcher.__name__}() in {timeout}s"
|
|
649
|
-
) from
|
|
645
|
+
) from exc
|
|
650
646
|
|
|
651
647
|
return status
|
|
652
648
|
|
|
@@ -184,7 +184,9 @@ def _datakey_shape(value: SignalDatatype) -> list[int | None]:
|
|
|
184
184
|
elif isinstance(value, Sequence | Table):
|
|
185
185
|
return [len(value)]
|
|
186
186
|
else:
|
|
187
|
-
raise TypeError(
|
|
187
|
+
raise TypeError(
|
|
188
|
+
f"Can't make shape for {value} with SignalDataType: {type(value)}"
|
|
189
|
+
)
|
|
188
190
|
|
|
189
191
|
|
|
190
192
|
def make_datakey(
|
ophyd_async/core/_status.py
CHANGED
ophyd_async/core/_table.py
CHANGED
|
@@ -13,6 +13,7 @@ TableSubclass = TypeVar("TableSubclass", bound="Table")
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def _concat(value1, value2):
|
|
16
|
+
"""Concatenate two values, supports both NumPy arrays and generic types."""
|
|
16
17
|
if isinstance(value1, np.ndarray):
|
|
17
18
|
return np.concatenate((value1, value2))
|
|
18
19
|
else:
|
|
@@ -20,6 +21,8 @@ def _concat(value1, value2):
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def _make_default_factory(dtype: np.dtype) -> Callable[[], np.ndarray]:
|
|
24
|
+
"""Creates a default factory, returns an empty Numpy array of a specified dtype."""
|
|
25
|
+
|
|
23
26
|
def numpy_array_default_factory() -> np.ndarray:
|
|
24
27
|
return np.array([], dtype)
|
|
25
28
|
|
|
@@ -91,6 +94,11 @@ class Table(ConfinedModel):
|
|
|
91
94
|
raise TypeError(f"Cannot use annotation {anno} in a Table")
|
|
92
95
|
cls.__annotations__[k] = new_anno
|
|
93
96
|
|
|
97
|
+
@classmethod
|
|
98
|
+
def empty(cls: type[TableSubclass]) -> TableSubclass:
|
|
99
|
+
"""Makes an empty table with zero length columns."""
|
|
100
|
+
return cls() # type: ignore
|
|
101
|
+
|
|
94
102
|
def __add__(self, right: TableSubclass) -> TableSubclass:
|
|
95
103
|
"""Concatenate the arrays in field values."""
|
|
96
104
|
cls = type(right)
|
ophyd_async/core/_utils.py
CHANGED
|
@@ -38,7 +38,7 @@ class UppercaseNameEnumMeta(EnumMeta):
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
class AnyStringUppercaseNameEnumMeta(UppercaseNameEnumMeta):
|
|
41
|
-
def __call__(
|
|
41
|
+
def __call__(cls, value, *args, **kwargs): # type: ignore
|
|
42
42
|
"""Return given value if it is a string and not a member of the enum.
|
|
43
43
|
|
|
44
44
|
If the value is not a string or is an enum member, default enum behavior
|
|
@@ -54,7 +54,7 @@ class AnyStringUppercaseNameEnumMeta(UppercaseNameEnumMeta):
|
|
|
54
54
|
member.
|
|
55
55
|
|
|
56
56
|
"""
|
|
57
|
-
if isinstance(value, str) and not isinstance(value,
|
|
57
|
+
if isinstance(value, str) and not isinstance(value, cls):
|
|
58
58
|
return value
|
|
59
59
|
return super().__call__(value, *args, **kwargs)
|
|
60
60
|
|
|
@@ -85,11 +85,11 @@ timeout itself
|
|
|
85
85
|
CalculatableTimeout = float | None | Literal["CALCULATE_TIMEOUT"]
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
class
|
|
88
|
+
class NotConnectedError(Exception):
|
|
89
89
|
"""Exception to be raised if a `Device.connect` is cancelled.
|
|
90
90
|
|
|
91
91
|
:param errors:
|
|
92
|
-
Mapping of device name to Exception or another
|
|
92
|
+
Mapping of device name to Exception or another NotConnectedError.
|
|
93
93
|
Alternatively a string with the signal error text.
|
|
94
94
|
"""
|
|
95
95
|
|
|
@@ -106,7 +106,7 @@ class NotConnected(Exception):
|
|
|
106
106
|
return {}
|
|
107
107
|
|
|
108
108
|
def _format_sub_errors(self, name: str, error: Exception, indent="") -> str:
|
|
109
|
-
if isinstance(error,
|
|
109
|
+
if isinstance(error, NotConnectedError):
|
|
110
110
|
error_txt = ":" + error.format_error_string(indent + self._indent_width)
|
|
111
111
|
elif isinstance(error, Exception):
|
|
112
112
|
error_txt = ": " + err_str + "\n" if (err_str := str(error)) else "\n"
|
|
@@ -138,15 +138,15 @@ class NotConnected(Exception):
|
|
|
138
138
|
@classmethod
|
|
139
139
|
def with_other_exceptions_logged(
|
|
140
140
|
cls, exceptions: Mapping[str, Exception]
|
|
141
|
-
) ->
|
|
141
|
+
) -> NotConnectedError:
|
|
142
142
|
for name, exception in exceptions.items():
|
|
143
|
-
if not isinstance(exception,
|
|
143
|
+
if not isinstance(exception, NotConnectedError):
|
|
144
144
|
logger.exception(
|
|
145
145
|
f"device `{name}` raised unexpected exception "
|
|
146
146
|
f"{type(exception).__name__}",
|
|
147
147
|
exc_info=exception,
|
|
148
148
|
)
|
|
149
|
-
return
|
|
149
|
+
return NotConnectedError(exceptions)
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
@dataclass(frozen=True)
|
|
@@ -192,8 +192,8 @@ async def wait_for_connection(**coros: Awaitable[None]):
|
|
|
192
192
|
name, coro = coros.popitem()
|
|
193
193
|
try:
|
|
194
194
|
await coro
|
|
195
|
-
except Exception as
|
|
196
|
-
exceptions[name] =
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
exceptions[name] = exc
|
|
197
197
|
else:
|
|
198
198
|
# Use gather to connect in parallel
|
|
199
199
|
results = await asyncio.gather(*coros.values(), return_exceptions=True)
|
|
@@ -202,7 +202,7 @@ async def wait_for_connection(**coros: Awaitable[None]):
|
|
|
202
202
|
exceptions[name] = result
|
|
203
203
|
|
|
204
204
|
if exceptions:
|
|
205
|
-
raise
|
|
205
|
+
raise NotConnectedError.with_other_exceptions_logged(exceptions)
|
|
206
206
|
|
|
207
207
|
|
|
208
208
|
def get_dtype(datatype: type) -> np.dtype:
|
|
@@ -34,9 +34,11 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
|
|
|
34
34
|
self,
|
|
35
35
|
driver: ADBaseIOT,
|
|
36
36
|
good_states: frozenset[ADState] = DEFAULT_GOOD_STATES,
|
|
37
|
+
image_mode: ADImageMode = ADImageMode.MULTIPLE,
|
|
37
38
|
) -> None:
|
|
38
39
|
self.driver: ADBaseIOT = driver
|
|
39
40
|
self.good_states = good_states
|
|
41
|
+
self.image_mode = image_mode
|
|
40
42
|
self.frame_timeout = DEFAULT_TIMEOUT
|
|
41
43
|
self._arm_status: AsyncStatus | None = None
|
|
42
44
|
|
|
@@ -52,7 +54,7 @@ class ADBaseController(DetectorController, Generic[ADBaseIOT]):
|
|
|
52
54
|
)
|
|
53
55
|
await asyncio.gather(
|
|
54
56
|
self.driver.num_images.set(trigger_info.total_number_of_exposures),
|
|
55
|
-
self.driver.image_mode.set(
|
|
57
|
+
self.driver.image_mode.set(self.image_mode),
|
|
56
58
|
)
|
|
57
59
|
|
|
58
60
|
async def arm(self):
|
|
@@ -63,8 +63,8 @@ def convert_pv_dtype_to_np(datatype: str) -> str:
|
|
|
63
63
|
else:
|
|
64
64
|
try:
|
|
65
65
|
np_datatype = convert_ad_dtype_to_np(_pvattribute_to_ad_datatype[datatype])
|
|
66
|
-
except KeyError as
|
|
67
|
-
raise ValueError(f"Invalid dbr type {datatype}") from
|
|
66
|
+
except KeyError as exc:
|
|
67
|
+
raise ValueError(f"Invalid dbr type {datatype}") from exc
|
|
68
68
|
return np_datatype
|
|
69
69
|
|
|
70
70
|
|
|
@@ -81,8 +81,8 @@ def convert_param_dtype_to_np(datatype: str) -> str:
|
|
|
81
81
|
np_datatype = convert_ad_dtype_to_np(
|
|
82
82
|
_paramattribute_to_ad_datatype[datatype]
|
|
83
83
|
)
|
|
84
|
-
except KeyError as
|
|
85
|
-
raise ValueError(f"Invalid datatype {datatype}") from
|
|
84
|
+
except KeyError as exc:
|
|
85
|
+
raise ValueError(f"Invalid datatype {datatype}") from exc
|
|
86
86
|
return np_datatype
|
|
87
87
|
|
|
88
88
|
|
ophyd_async/epics/core/_aioca.py
CHANGED
|
@@ -26,7 +26,7 @@ from event_model import DataKey, Limits, LimitsRange
|
|
|
26
26
|
from ophyd_async.core import (
|
|
27
27
|
Array1D,
|
|
28
28
|
Callback,
|
|
29
|
-
|
|
29
|
+
NotConnectedError,
|
|
30
30
|
SignalDatatype,
|
|
31
31
|
SignalDatatypeT,
|
|
32
32
|
SignalMetadata,
|
|
@@ -272,7 +272,7 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
272
272
|
)
|
|
273
273
|
except CANothing as exc:
|
|
274
274
|
logger.debug(f"signal ca://{pv} timed out")
|
|
275
|
-
raise
|
|
275
|
+
raise NotConnectedError(f"ca://{pv}") from exc
|
|
276
276
|
|
|
277
277
|
async def connect(self, timeout: float):
|
|
278
278
|
_use_pyepics_context_if_imported()
|
ophyd_async/epics/core/_p4p.py
CHANGED
|
@@ -18,7 +18,7 @@ from pydantic import BaseModel
|
|
|
18
18
|
from ophyd_async.core import (
|
|
19
19
|
Array1D,
|
|
20
20
|
Callback,
|
|
21
|
-
|
|
21
|
+
NotConnectedError,
|
|
22
22
|
SignalDatatype,
|
|
23
23
|
SignalDatatypeT,
|
|
24
24
|
SignalMetadata,
|
|
@@ -328,7 +328,7 @@ async def pvget_with_timeout(pv: str, timeout: float) -> Any:
|
|
|
328
328
|
return await asyncio.wait_for(context().get(pv), timeout=timeout)
|
|
329
329
|
except TimeoutError as exc:
|
|
330
330
|
logger.debug(f"signal pva://{pv} timed out", exc_info=True)
|
|
331
|
-
raise
|
|
331
|
+
raise NotConnectedError(f"pva://{pv}") from exc
|
|
332
332
|
|
|
333
333
|
|
|
334
334
|
def _pva_request_string(fields: Sequence[str]) -> str:
|
ophyd_async/epics/motor.py
CHANGED
|
@@ -32,15 +32,34 @@ from ophyd_async.core import (
|
|
|
32
32
|
from ophyd_async.core import StandardReadableFormat as Format
|
|
33
33
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
|
|
34
34
|
|
|
35
|
-
__all__ = ["
|
|
35
|
+
__all__ = ["MotorLimitsError", "Motor"]
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
class
|
|
38
|
+
class MotorLimitsError(Exception):
|
|
39
39
|
"""Exception for invalid motor limits."""
|
|
40
40
|
|
|
41
41
|
pass
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
# Back compat - delete before 1.0
|
|
45
|
+
def __getattr__(name):
|
|
46
|
+
import warnings
|
|
47
|
+
|
|
48
|
+
renames = {
|
|
49
|
+
"MotorLimitsException": MotorLimitsError,
|
|
50
|
+
}
|
|
51
|
+
rename = renames.get(name)
|
|
52
|
+
if rename is not None:
|
|
53
|
+
warnings.warn(
|
|
54
|
+
DeprecationWarning(
|
|
55
|
+
f"{name!r} is deprecated, use {rename.__name__!r} instead"
|
|
56
|
+
),
|
|
57
|
+
stacklevel=2,
|
|
58
|
+
)
|
|
59
|
+
return rename
|
|
60
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
61
|
+
|
|
62
|
+
|
|
44
63
|
class OffsetMode(StrictEnum):
|
|
45
64
|
VARIABLE = "Variable"
|
|
46
65
|
FROZEN = "Frozen"
|
|
@@ -126,7 +145,7 @@ class Motor(
|
|
|
126
145
|
not motor_upper_limit >= abs_start_pos >= motor_lower_limit
|
|
127
146
|
or not motor_upper_limit >= abs_end_pos >= motor_lower_limit
|
|
128
147
|
):
|
|
129
|
-
raise
|
|
148
|
+
raise MotorLimitsError(
|
|
130
149
|
f"{self.name} motor trajectory for requested fly/move is from "
|
|
131
150
|
f"{abs_start_pos}{egu} to "
|
|
132
151
|
f"{abs_end_pos}{egu} but motor limits are "
|
|
@@ -144,7 +163,7 @@ class Motor(
|
|
|
144
163
|
self.max_velocity.get_value(), self.motor_egu.get_value()
|
|
145
164
|
)
|
|
146
165
|
if abs(value.velocity) > max_speed:
|
|
147
|
-
raise
|
|
166
|
+
raise MotorLimitsError(
|
|
148
167
|
f"Velocity {abs(value.velocity)} {egu}/s was requested for a motor "
|
|
149
168
|
f" with max speed of {max_speed} {egu}/s"
|
|
150
169
|
)
|
|
@@ -242,9 +261,11 @@ class Motor(
|
|
|
242
261
|
)
|
|
243
262
|
return Location(setpoint=setpoint, readback=readback)
|
|
244
263
|
|
|
245
|
-
def
|
|
246
|
-
"""Subscribe."""
|
|
247
|
-
self.user_readback.
|
|
264
|
+
def subscribe_reading(self, function: Callback[dict[str, Reading[float]]]) -> None:
|
|
265
|
+
"""Subscribe to reading."""
|
|
266
|
+
self.user_readback.subscribe_reading(function)
|
|
267
|
+
|
|
268
|
+
subscribe = subscribe_reading
|
|
248
269
|
|
|
249
270
|
def clear_sub(self, function: Callback[dict[str, Reading[float]]]) -> None:
|
|
250
271
|
"""Unsubscribe."""
|
|
@@ -4,7 +4,7 @@ import numpy as np
|
|
|
4
4
|
|
|
5
5
|
from ophyd_async.core import Array1D, Device, DeviceVector, StandardReadable
|
|
6
6
|
from ophyd_async.epics import motor
|
|
7
|
-
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
|
|
7
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
8
8
|
|
|
9
9
|
CS_LETTERS = "ABCUVWXYZ"
|
|
10
10
|
|
|
@@ -40,11 +40,15 @@ class PmacTrajectoryIO(StandardReadable):
|
|
|
40
40
|
for i, letter in enumerate(CS_LETTERS)
|
|
41
41
|
}
|
|
42
42
|
)
|
|
43
|
+
self.total_points = epics_signal_r(int, f"{prefix}TotalPoints_RBV")
|
|
43
44
|
self.points_to_build = epics_signal_rw(int, prefix + "ProfilePointsToBuild")
|
|
44
|
-
self.build_profile =
|
|
45
|
+
self.build_profile = epics_signal_x(prefix + "ProfileBuild")
|
|
46
|
+
self.append_profile = epics_signal_x(prefix + "ProfileAppend")
|
|
47
|
+
# This should be a SignalX, but because it is a Busy record, must
|
|
48
|
+
# be a SignalRW to be waited on in PmacTrajectoryTriggerLogic.
|
|
49
|
+
# TODO: Change record type to bo from busy (https://github.com/DiamondLightSource/pmac/issues/154)
|
|
45
50
|
self.execute_profile = epics_signal_rw(bool, prefix + "ProfileExecute")
|
|
46
|
-
self.
|
|
47
|
-
self.abort_profile = epics_signal_rw(bool, prefix + "ProfileAbort")
|
|
51
|
+
self.abort_profile = epics_signal_x(prefix + "ProfileAbort")
|
|
48
52
|
self.profile_cs_name = epics_signal_rw(str, prefix + "ProfileCsName")
|
|
49
53
|
self.calculate_velocities = epics_signal_rw(bool, prefix + "ProfileCalcVel")
|
|
50
54
|
|