ophyd-async 0.3a5__py3-none-any.whl → 0.3a6__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 +1 -1
- ophyd_async/core/__init__.py +4 -0
- ophyd_async/core/async_status.py +11 -28
- ophyd_async/core/detector.py +1 -1
- ophyd_async/core/device.py +2 -4
- ophyd_async/core/flyer.py +1 -1
- ophyd_async/core/mock_signal_utils.py +2 -6
- ophyd_async/core/signal.py +23 -17
- ophyd_async/core/utils.py +11 -0
- ophyd_async/epics/areadetector/drivers/ad_base.py +1 -7
- ophyd_async/epics/areadetector/writers/hdf_writer.py +3 -2
- ophyd_async/epics/areadetector/writers/nd_file_hdf.py +0 -2
- ophyd_async/epics/areadetector/writers/nd_plugin.py +9 -0
- ophyd_async/epics/demo/__init__.py +30 -26
- ophyd_async/epics/motion/motor.py +35 -29
- ophyd_async/panda/__init__.py +2 -0
- ophyd_async/panda/writers/_hdf_writer.py +4 -4
- ophyd_async/{planstubs → plan_stubs}/__init__.py +5 -1
- ophyd_async/plan_stubs/fly.py +149 -0
- ophyd_async/sim/demo/sim_motor.py +56 -86
- {ophyd_async-0.3a5.dist-info → ophyd_async-0.3a6.dist-info}/METADATA +1 -1
- {ophyd_async-0.3a5.dist-info → ophyd_async-0.3a6.dist-info}/RECORD +27 -27
- ophyd_async/planstubs/prepare_trigger_and_dets.py +0 -57
- /ophyd_async/{planstubs → plan_stubs}/ensure_connected.py +0 -0
- {ophyd_async-0.3a5.dist-info → ophyd_async-0.3a6.dist-info}/LICENSE +0 -0
- {ophyd_async-0.3a5.dist-info → ophyd_async-0.3a6.dist-info}/WHEEL +0 -0
- {ophyd_async-0.3a5.dist-info → ophyd_async-0.3a6.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.3a5.dist-info → ophyd_async-0.3a6.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py
CHANGED
ophyd_async/core/__init__.py
CHANGED
|
@@ -57,6 +57,8 @@ from .soft_signal_backend import SoftSignalBackend
|
|
|
57
57
|
from .standard_readable import ConfigSignal, HintedSignal, StandardReadable
|
|
58
58
|
from .utils import (
|
|
59
59
|
DEFAULT_TIMEOUT,
|
|
60
|
+
CalculatableTimeout,
|
|
61
|
+
CalculateTimeout,
|
|
60
62
|
Callback,
|
|
61
63
|
NotConnected,
|
|
62
64
|
ReadingValueCallback,
|
|
@@ -108,6 +110,8 @@ __all__ = [
|
|
|
108
110
|
"TriggerInfo",
|
|
109
111
|
"TriggerLogic",
|
|
110
112
|
"HardwareTriggeredFlyable",
|
|
113
|
+
"CalculateTimeout",
|
|
114
|
+
"CalculatableTimeout",
|
|
111
115
|
"DEFAULT_TIMEOUT",
|
|
112
116
|
"Callback",
|
|
113
117
|
"NotConnected",
|
ophyd_async/core/async_status.py
CHANGED
|
@@ -9,7 +9,6 @@ from typing import (
|
|
|
9
9
|
Awaitable,
|
|
10
10
|
Callable,
|
|
11
11
|
Generic,
|
|
12
|
-
SupportsFloat,
|
|
13
12
|
Type,
|
|
14
13
|
TypeVar,
|
|
15
14
|
cast,
|
|
@@ -27,15 +26,11 @@ WAS = TypeVar("WAS", bound="WatchableAsyncStatus")
|
|
|
27
26
|
class AsyncStatusBase(Status):
|
|
28
27
|
"""Convert asyncio awaitable to bluesky Status interface"""
|
|
29
28
|
|
|
30
|
-
def __init__(self, awaitable: Awaitable
|
|
31
|
-
if isinstance(timeout, SupportsFloat):
|
|
32
|
-
timeout = float(timeout)
|
|
29
|
+
def __init__(self, awaitable: Awaitable):
|
|
33
30
|
if isinstance(awaitable, asyncio.Task):
|
|
34
31
|
self.task = awaitable
|
|
35
32
|
else:
|
|
36
|
-
self.task = asyncio.create_task(
|
|
37
|
-
asyncio.wait_for(awaitable, timeout=timeout)
|
|
38
|
-
)
|
|
33
|
+
self.task = asyncio.create_task(awaitable)
|
|
39
34
|
self.task.add_done_callback(self._run_callbacks)
|
|
40
35
|
self._callbacks: list[Callback[Status]] = []
|
|
41
36
|
|
|
@@ -49,9 +44,8 @@ class AsyncStatusBase(Status):
|
|
|
49
44
|
self._callbacks.append(callback)
|
|
50
45
|
|
|
51
46
|
def _run_callbacks(self, task: asyncio.Task):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
callback(self)
|
|
47
|
+
for callback in self._callbacks:
|
|
48
|
+
callback(self)
|
|
55
49
|
|
|
56
50
|
def exception(self, timeout: float | None = 0.0) -> BaseException | None:
|
|
57
51
|
if timeout != 0.0:
|
|
@@ -93,14 +87,11 @@ class AsyncStatusBase(Status):
|
|
|
93
87
|
class AsyncStatus(AsyncStatusBase):
|
|
94
88
|
@classmethod
|
|
95
89
|
def wrap(cls: Type[AS], f: Callable[P, Awaitable]) -> Callable[P, AS]:
|
|
90
|
+
"""Wrap an async function in an AsyncStatus."""
|
|
91
|
+
|
|
96
92
|
@functools.wraps(f)
|
|
97
93
|
def wrap_f(*args: P.args, **kwargs: P.kwargs) -> AS:
|
|
98
|
-
|
|
99
|
-
# yet support keywords
|
|
100
|
-
# https://peps.python.org/pep-0612/#concatenating-keyword-parameters
|
|
101
|
-
timeout = kwargs.get("timeout")
|
|
102
|
-
assert isinstance(timeout, SupportsFloat) or timeout is None
|
|
103
|
-
return cls(f(*args, **kwargs), timeout=timeout)
|
|
94
|
+
return cls(f(*args, **kwargs))
|
|
104
95
|
|
|
105
96
|
# type is actually functools._Wrapped[P, Awaitable, P, AS]
|
|
106
97
|
# but functools._Wrapped is not necessarily available
|
|
@@ -110,15 +101,11 @@ class AsyncStatus(AsyncStatusBase):
|
|
|
110
101
|
class WatchableAsyncStatus(AsyncStatusBase, Generic[T]):
|
|
111
102
|
"""Convert AsyncIterator of WatcherUpdates to bluesky Status interface."""
|
|
112
103
|
|
|
113
|
-
def __init__(
|
|
114
|
-
self,
|
|
115
|
-
iterator: AsyncIterator[WatcherUpdate[T]],
|
|
116
|
-
timeout: SupportsFloat | None = None,
|
|
117
|
-
):
|
|
104
|
+
def __init__(self, iterator: AsyncIterator[WatcherUpdate[T]]):
|
|
118
105
|
self._watchers: list[Watcher] = []
|
|
119
106
|
self._start = time.monotonic()
|
|
120
107
|
self._last_update: WatcherUpdate[T] | None = None
|
|
121
|
-
super().__init__(self._notify_watchers_from(iterator)
|
|
108
|
+
super().__init__(self._notify_watchers_from(iterator))
|
|
122
109
|
|
|
123
110
|
async def _notify_watchers_from(self, iterator: AsyncIterator[WatcherUpdate[T]]):
|
|
124
111
|
async for update in iterator:
|
|
@@ -146,14 +133,10 @@ class WatchableAsyncStatus(AsyncStatusBase, Generic[T]):
|
|
|
146
133
|
cls: Type[WAS],
|
|
147
134
|
f: Callable[P, AsyncIterator[WatcherUpdate[T]]],
|
|
148
135
|
) -> Callable[P, WAS]:
|
|
149
|
-
"""Wrap an AsyncIterator in a WatchableAsyncStatus.
|
|
150
|
-
'timeout' as an argument, this must be a float or None, and it
|
|
151
|
-
will be propagated to the status."""
|
|
136
|
+
"""Wrap an AsyncIterator in a WatchableAsyncStatus."""
|
|
152
137
|
|
|
153
138
|
@functools.wraps(f)
|
|
154
139
|
def wrap_f(*args: P.args, **kwargs: P.kwargs) -> WAS:
|
|
155
|
-
|
|
156
|
-
assert isinstance(timeout, SupportsFloat) or timeout is None
|
|
157
|
-
return cls(f(*args, **kwargs), timeout=timeout)
|
|
140
|
+
return cls(f(*args, **kwargs))
|
|
158
141
|
|
|
159
142
|
return cast(Callable[P, WAS], wrap_f)
|
ophyd_async/core/detector.py
CHANGED
|
@@ -332,7 +332,7 @@ class StandardDetector(
|
|
|
332
332
|
# Collect stream datum documents for all indices written.
|
|
333
333
|
# The index is optional, and provided for fly scans, however this needs to be
|
|
334
334
|
# retrieved for step scans.
|
|
335
|
-
if
|
|
335
|
+
if index is None:
|
|
336
336
|
index = await self.writer.get_indices_written()
|
|
337
337
|
async for doc in self.writer.collect_stream_docs(index):
|
|
338
338
|
yield doc
|
ophyd_async/core/device.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Base device"""
|
|
2
2
|
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
3
|
import asyncio
|
|
6
4
|
import sys
|
|
7
5
|
from functools import cached_property
|
|
@@ -32,7 +30,7 @@ class Device(HasName):
|
|
|
32
30
|
|
|
33
31
|
_name: str = ""
|
|
34
32
|
#: The parent Device if it exists
|
|
35
|
-
parent: Optional[Device] = None
|
|
33
|
+
parent: Optional["Device"] = None
|
|
36
34
|
# None if connect hasn't started, a Task if it has
|
|
37
35
|
_connect_task: Optional[asyncio.Task] = None
|
|
38
36
|
_connect_mock_arg: bool = False
|
|
@@ -51,7 +49,7 @@ class Device(HasName):
|
|
|
51
49
|
getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
|
|
52
50
|
)
|
|
53
51
|
|
|
54
|
-
def children(self) -> Iterator[Tuple[str, Device]]:
|
|
52
|
+
def children(self) -> Iterator[Tuple[str, "Device"]]:
|
|
55
53
|
for attr_name, attr in self.__dict__.items():
|
|
56
54
|
if attr_name != "parent" and isinstance(attr, Device):
|
|
57
55
|
yield attr_name, attr
|
ophyd_async/core/flyer.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from contextlib import asynccontextmanager, contextmanager
|
|
2
|
-
from typing import Any, Callable,
|
|
2
|
+
from typing import Any, Callable, Iterable, Iterator, List
|
|
3
3
|
from unittest.mock import ANY, Mock
|
|
4
4
|
|
|
5
5
|
from ophyd_async.core.signal import Signal
|
|
@@ -127,11 +127,7 @@ def _unset_side_effect_cm(put_mock: Mock):
|
|
|
127
127
|
put_mock.side_effect = None
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
# ContextManager[None]
|
|
132
|
-
def callback_on_mock_put(
|
|
133
|
-
signal: Signal, callback: Callable[[T], None]
|
|
134
|
-
) -> Generator[None, None, None]:
|
|
130
|
+
def callback_on_mock_put(signal: Signal, callback: Callable[[T], None]):
|
|
135
131
|
"""For setting a callback when a backend is put to.
|
|
136
132
|
|
|
137
133
|
Can either be used in a context, with the callback being
|
ophyd_async/core/signal.py
CHANGED
|
@@ -32,7 +32,7 @@ from .async_status import AsyncStatus
|
|
|
32
32
|
from .device import Device
|
|
33
33
|
from .signal_backend import SignalBackend
|
|
34
34
|
from .soft_signal_backend import SoftSignalBackend
|
|
35
|
-
from .utils import DEFAULT_TIMEOUT, Callback, T
|
|
35
|
+
from .utils import DEFAULT_TIMEOUT, CalculatableTimeout, CalculateTimeout, Callback, T
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def _add_timeout(func):
|
|
@@ -213,15 +213,14 @@ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
|
|
|
213
213
|
self._del_cache(self._get_cache().set_staged(False))
|
|
214
214
|
|
|
215
215
|
|
|
216
|
-
USE_DEFAULT_TIMEOUT = "USE_DEFAULT_TIMEOUT"
|
|
217
|
-
|
|
218
|
-
|
|
219
216
|
class SignalW(Signal[T], Movable):
|
|
220
217
|
"""Signal that can be set"""
|
|
221
218
|
|
|
222
|
-
def set(
|
|
219
|
+
def set(
|
|
220
|
+
self, value: T, wait=True, timeout: CalculatableTimeout = CalculateTimeout
|
|
221
|
+
) -> AsyncStatus:
|
|
223
222
|
"""Set the value and return a status saying when it's done"""
|
|
224
|
-
if timeout is
|
|
223
|
+
if timeout is CalculateTimeout:
|
|
225
224
|
timeout = self._timeout
|
|
226
225
|
|
|
227
226
|
async def do_set():
|
|
@@ -248,9 +247,11 @@ class SignalRW(SignalR[T], SignalW[T], Locatable):
|
|
|
248
247
|
class SignalX(Signal):
|
|
249
248
|
"""Signal that puts the default value"""
|
|
250
249
|
|
|
251
|
-
def trigger(
|
|
250
|
+
def trigger(
|
|
251
|
+
self, wait=True, timeout: CalculatableTimeout = CalculateTimeout
|
|
252
|
+
) -> AsyncStatus:
|
|
252
253
|
"""Trigger the action and return a status saying when it's done"""
|
|
253
|
-
if timeout is
|
|
254
|
+
if timeout is CalculateTimeout:
|
|
254
255
|
timeout = self._timeout
|
|
255
256
|
coro = self._backend.put(None, wait=wait, timeout=timeout)
|
|
256
257
|
return AsyncStatus(coro)
|
|
@@ -270,7 +271,7 @@ def soft_signal_r_and_setter(
|
|
|
270
271
|
datatype: Optional[Type[T]] = None,
|
|
271
272
|
initial_value: Optional[T] = None,
|
|
272
273
|
name: str = "",
|
|
273
|
-
) -> Tuple[SignalR[T], Callable[[T]]]:
|
|
274
|
+
) -> Tuple[SignalR[T], Callable[[T], None]]:
|
|
274
275
|
"""Returns a tuple of a read-only Signal and a callable through
|
|
275
276
|
which the signal can be internally modified within the device. Use
|
|
276
277
|
soft_signal_rw if you want a device that is externally modifiable
|
|
@@ -394,7 +395,7 @@ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
|
|
|
394
395
|
|
|
395
396
|
|
|
396
397
|
async def observe_value(
|
|
397
|
-
signal: SignalR[T], timeout=None, done_status: Status | None = None
|
|
398
|
+
signal: SignalR[T], timeout: float | None = None, done_status: Status | None = None
|
|
398
399
|
) -> AsyncGenerator[T, None]:
|
|
399
400
|
"""Subscribe to the value of a signal so it can be iterated from.
|
|
400
401
|
|
|
@@ -403,8 +404,12 @@ async def observe_value(
|
|
|
403
404
|
signal:
|
|
404
405
|
Call subscribe_value on this at the start, and clear_sub on it at the
|
|
405
406
|
end
|
|
407
|
+
timeout:
|
|
408
|
+
If given, how long to wait for each updated value in seconds. If an update
|
|
409
|
+
is not produced in this time then raise asyncio.TimeoutError
|
|
406
410
|
done_status:
|
|
407
411
|
If this status is complete, stop observing and make the iterator return.
|
|
412
|
+
If it raises an exception then this exception will be raised by the iterator.
|
|
408
413
|
|
|
409
414
|
Notes
|
|
410
415
|
-----
|
|
@@ -414,9 +419,7 @@ async def observe_value(
|
|
|
414
419
|
do_something_with(value)
|
|
415
420
|
"""
|
|
416
421
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
q: asyncio.Queue[T | StatusIsDone] = asyncio.Queue()
|
|
422
|
+
q: asyncio.Queue[T | Status] = asyncio.Queue()
|
|
420
423
|
if timeout is None:
|
|
421
424
|
get_value = q.get
|
|
422
425
|
else:
|
|
@@ -425,16 +428,19 @@ async def observe_value(
|
|
|
425
428
|
return await asyncio.wait_for(q.get(), timeout)
|
|
426
429
|
|
|
427
430
|
if done_status is not None:
|
|
428
|
-
done_status.add_callback(
|
|
431
|
+
done_status.add_callback(q.put_nowait)
|
|
429
432
|
|
|
430
433
|
signal.subscribe_value(q.put_nowait)
|
|
431
434
|
try:
|
|
432
435
|
while True:
|
|
433
436
|
item = await get_value()
|
|
434
|
-
if
|
|
435
|
-
|
|
437
|
+
if done_status and item is done_status:
|
|
438
|
+
if exc := done_status.exception():
|
|
439
|
+
raise exc
|
|
440
|
+
else:
|
|
441
|
+
break
|
|
436
442
|
else:
|
|
437
|
-
|
|
443
|
+
yield item
|
|
438
444
|
finally:
|
|
439
445
|
signal.clear_sub(q.put_nowait)
|
|
440
446
|
|
ophyd_async/core/utils.py
CHANGED
|
@@ -31,6 +31,17 @@ DEFAULT_TIMEOUT = 10.0
|
|
|
31
31
|
ErrorText = Union[str, Dict[str, Exception]]
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class CalculateTimeout:
|
|
35
|
+
"""Sentinel class used to implement ``myfunc(timeout=CalculateTimeout)``
|
|
36
|
+
|
|
37
|
+
This signifies that the function should calculate a suitable non-zero
|
|
38
|
+
timeout itself
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
CalculatableTimeout = float | None | Type[CalculateTimeout]
|
|
43
|
+
|
|
44
|
+
|
|
34
45
|
class NotConnected(Exception):
|
|
35
46
|
"""Exception to be raised if a `Device.connect` is cancelled"""
|
|
36
47
|
|
|
@@ -9,7 +9,7 @@ from ophyd_async.core import (
|
|
|
9
9
|
set_and_wait_for_value,
|
|
10
10
|
)
|
|
11
11
|
|
|
12
|
-
from ...signal.signal import epics_signal_r,
|
|
12
|
+
from ...signal.signal import epics_signal_r, epics_signal_rw_rbv
|
|
13
13
|
from ..utils import ImageMode
|
|
14
14
|
from ..writers.nd_plugin import NDArrayBase
|
|
15
15
|
|
|
@@ -43,18 +43,12 @@ DEFAULT_GOOD_STATES: FrozenSet[DetectorState] = frozenset(
|
|
|
43
43
|
class ADBase(NDArrayBase):
|
|
44
44
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
45
45
|
# Define some signals
|
|
46
|
-
self.acquire = epics_signal_rw_rbv(bool, prefix + "Acquire")
|
|
47
46
|
self.acquire_time = epics_signal_rw_rbv(float, prefix + "AcquireTime")
|
|
48
47
|
self.num_images = epics_signal_rw_rbv(int, prefix + "NumImages")
|
|
49
48
|
self.image_mode = epics_signal_rw_rbv(ImageMode, prefix + "ImageMode")
|
|
50
|
-
self.array_counter = epics_signal_rw_rbv(int, prefix + "ArrayCounter")
|
|
51
|
-
self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV")
|
|
52
|
-
self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV")
|
|
53
49
|
self.detector_state = epics_signal_r(
|
|
54
50
|
DetectorState, prefix + "DetectorState_RBV"
|
|
55
51
|
)
|
|
56
|
-
# There is no _RBV for this one
|
|
57
|
-
self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins")
|
|
58
52
|
super().__init__(prefix, name=name)
|
|
59
53
|
|
|
60
54
|
|
|
@@ -43,12 +43,13 @@ class HDFWriter(DetectorWriter):
|
|
|
43
43
|
async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
|
|
44
44
|
self._file = None
|
|
45
45
|
info = self._directory_provider()
|
|
46
|
+
file_path = str(info.root / info.resource_dir)
|
|
46
47
|
await asyncio.gather(
|
|
47
48
|
self.hdf.num_extra_dims.set(0),
|
|
48
49
|
self.hdf.lazy_open.set(True),
|
|
49
50
|
self.hdf.swmr_mode.set(True),
|
|
50
51
|
# See https://github.com/bluesky/ophyd-async/issues/122
|
|
51
|
-
self.hdf.file_path.set(
|
|
52
|
+
self.hdf.file_path.set(file_path),
|
|
52
53
|
self.hdf.file_name.set(f"{info.prefix}{self.hdf.name}{info.suffix}"),
|
|
53
54
|
self.hdf.file_template.set("%s/%s.h5"),
|
|
54
55
|
self.hdf.file_write_mode.set(FileWriteMode.stream),
|
|
@@ -59,7 +60,7 @@ class HDFWriter(DetectorWriter):
|
|
|
59
60
|
|
|
60
61
|
assert (
|
|
61
62
|
await self.hdf.file_path_exists.get_value()
|
|
62
|
-
), f"File path {
|
|
63
|
+
), f"File path {file_path} for hdf plugin does not exist"
|
|
63
64
|
|
|
64
65
|
# Overwrite num_capture to go forever
|
|
65
66
|
await self.hdf.num_capture.set(0)
|
|
@@ -36,7 +36,5 @@ class NDFileHDF(NDPluginBase):
|
|
|
36
36
|
self.lazy_open = epics_signal_rw_rbv(bool, prefix + "LazyOpen")
|
|
37
37
|
self.capture = epics_signal_rw_rbv(bool, prefix + "Capture")
|
|
38
38
|
self.flush_now = epics_signal_rw(bool, prefix + "FlushNow")
|
|
39
|
-
self.array_size0 = epics_signal_r(int, prefix + "ArraySize0_RBV")
|
|
40
|
-
self.array_size1 = epics_signal_r(int, prefix + "ArraySize1_RBV")
|
|
41
39
|
self.xml_file_name = epics_signal_rw_rbv(str, prefix + "XMLFileName")
|
|
42
40
|
super().__init__(prefix, name)
|
|
@@ -14,6 +14,13 @@ class NDArrayBase(Device):
|
|
|
14
14
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
15
15
|
self.unique_id = epics_signal_r(int, prefix + "UniqueId_RBV")
|
|
16
16
|
self.nd_attributes_file = epics_signal_rw(str, prefix + "NDAttributesFile")
|
|
17
|
+
self.acquire = epics_signal_rw_rbv(bool, prefix + "Acquire")
|
|
18
|
+
self.array_size_x = epics_signal_r(int, prefix + "ArraySizeX_RBV")
|
|
19
|
+
self.array_size_y = epics_signal_r(int, prefix + "ArraySizeY_RBV")
|
|
20
|
+
self.array_counter = epics_signal_rw_rbv(int, prefix + "ArrayCounter")
|
|
21
|
+
# There is no _RBV for this one
|
|
22
|
+
self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins")
|
|
23
|
+
|
|
17
24
|
super().__init__(name=name)
|
|
18
25
|
|
|
19
26
|
|
|
@@ -22,6 +29,8 @@ class NDPluginBase(NDArrayBase):
|
|
|
22
29
|
self.nd_array_port = epics_signal_rw_rbv(str, prefix + "NDArrayPort")
|
|
23
30
|
self.enable_callback = epics_signal_rw_rbv(Callback, prefix + "EnableCallbacks")
|
|
24
31
|
self.nd_array_address = epics_signal_rw_rbv(int, prefix + "NDArrayAddress")
|
|
32
|
+
self.array_size0 = epics_signal_r(int, prefix + "ArraySize0_RBV")
|
|
33
|
+
self.array_size1 = epics_signal_r(int, prefix + "ArraySize1_RBV")
|
|
25
34
|
super().__init__(prefix, name)
|
|
26
35
|
|
|
27
36
|
|
|
@@ -6,7 +6,6 @@ import random
|
|
|
6
6
|
import string
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
|
-
from dataclasses import replace
|
|
10
9
|
from enum import Enum
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
|
|
@@ -22,7 +21,13 @@ from ophyd_async.core import (
|
|
|
22
21
|
WatchableAsyncStatus,
|
|
23
22
|
observe_value,
|
|
24
23
|
)
|
|
25
|
-
from ophyd_async.core.
|
|
24
|
+
from ophyd_async.core.async_status import AsyncStatus
|
|
25
|
+
from ophyd_async.core.utils import (
|
|
26
|
+
DEFAULT_TIMEOUT,
|
|
27
|
+
CalculatableTimeout,
|
|
28
|
+
CalculateTimeout,
|
|
29
|
+
WatcherUpdate,
|
|
30
|
+
)
|
|
26
31
|
|
|
27
32
|
from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
28
33
|
|
|
@@ -66,11 +71,9 @@ class Mover(StandardReadable, Movable, Stoppable):
|
|
|
66
71
|
# Define some signals
|
|
67
72
|
with self.add_children_as_readables(HintedSignal):
|
|
68
73
|
self.readback = epics_signal_r(float, prefix + "Readback")
|
|
69
|
-
|
|
70
74
|
with self.add_children_as_readables(ConfigSignal):
|
|
71
75
|
self.velocity = epics_signal_rw(float, prefix + "Velocity")
|
|
72
76
|
self.units = epics_signal_r(str, prefix + "Readback.EGU")
|
|
73
|
-
|
|
74
77
|
self.setpoint = epics_signal_rw(float, prefix + "Setpoint")
|
|
75
78
|
self.precision = epics_signal_r(int, prefix + "Readback.PREC")
|
|
76
79
|
# Signals that collide with standard methods should have a trailing underscore
|
|
@@ -85,41 +88,42 @@ class Mover(StandardReadable, Movable, Stoppable):
|
|
|
85
88
|
# Readback should be named the same as its parent in read()
|
|
86
89
|
self.readback.set_name(name)
|
|
87
90
|
|
|
88
|
-
|
|
91
|
+
@WatchableAsyncStatus.wrap
|
|
92
|
+
async def set(
|
|
93
|
+
self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
|
|
94
|
+
):
|
|
89
95
|
self._set_success = True
|
|
90
|
-
|
|
91
|
-
old_position, units, precision = await asyncio.gather(
|
|
96
|
+
old_position, units, precision, velocity = await asyncio.gather(
|
|
92
97
|
self.setpoint.get_value(),
|
|
93
98
|
self.units.get_value(),
|
|
94
99
|
self.precision.get_value(),
|
|
100
|
+
self.velocity.get_value(),
|
|
95
101
|
)
|
|
102
|
+
if timeout is CalculateTimeout:
|
|
103
|
+
assert velocity > 0, "Mover has zero velocity"
|
|
104
|
+
timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT
|
|
105
|
+
# Make an Event that will be set on completion, and a Status that will
|
|
106
|
+
# error if not done in time
|
|
107
|
+
done = asyncio.Event()
|
|
108
|
+
done_status = AsyncStatus(asyncio.wait_for(done.wait(), timeout))
|
|
96
109
|
# Wait for the value to set, but don't wait for put completion callback
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
await self.setpoint.set(new_position, wait=False)
|
|
111
|
+
async for current_position in observe_value(
|
|
112
|
+
self.readback, done_status=done_status
|
|
113
|
+
):
|
|
114
|
+
yield WatcherUpdate(
|
|
115
|
+
current=current_position,
|
|
103
116
|
initial=old_position,
|
|
104
|
-
current=old_position,
|
|
105
117
|
target=new_position,
|
|
118
|
+
name=self.name,
|
|
106
119
|
unit=units,
|
|
107
120
|
precision=precision,
|
|
108
|
-
),
|
|
109
|
-
move_status,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
@WatchableAsyncStatus.wrap # uses the timeout argument from the function it wraps
|
|
113
|
-
async def set(self, new_position: float, timeout: float | None = None):
|
|
114
|
-
update, _ = await self._move(new_position)
|
|
115
|
-
async for current_position in observe_value(self.readback):
|
|
116
|
-
yield replace(
|
|
117
|
-
update,
|
|
118
|
-
name=self.name,
|
|
119
|
-
current=current_position,
|
|
120
121
|
)
|
|
121
122
|
if np.isclose(current_position, new_position):
|
|
123
|
+
done.set()
|
|
122
124
|
break
|
|
125
|
+
if not self._set_success:
|
|
126
|
+
raise RuntimeError("Motor was stopped")
|
|
123
127
|
|
|
124
128
|
async def stop(self, success=True):
|
|
125
129
|
self._set_success = success
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from dataclasses import replace
|
|
3
2
|
|
|
4
3
|
from bluesky.protocols import Movable, Stoppable
|
|
5
4
|
|
|
6
5
|
from ophyd_async.core import (
|
|
7
|
-
AsyncStatus,
|
|
8
6
|
ConfigSignal,
|
|
9
7
|
HintedSignal,
|
|
10
8
|
StandardReadable,
|
|
11
9
|
WatchableAsyncStatus,
|
|
12
10
|
)
|
|
13
11
|
from ophyd_async.core.signal import observe_value
|
|
14
|
-
from ophyd_async.core.utils import
|
|
12
|
+
from ophyd_async.core.utils import (
|
|
13
|
+
DEFAULT_TIMEOUT,
|
|
14
|
+
CalculatableTimeout,
|
|
15
|
+
CalculateTimeout,
|
|
16
|
+
WatcherUpdate,
|
|
17
|
+
)
|
|
15
18
|
|
|
16
19
|
from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
17
20
|
|
|
@@ -47,42 +50,45 @@ class Motor(StandardReadable, Movable, Stoppable):
|
|
|
47
50
|
# Readback should be named the same as its parent in read()
|
|
48
51
|
self.user_readback.set_name(name)
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
@WatchableAsyncStatus.wrap
|
|
54
|
+
async def set(
|
|
55
|
+
self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
|
|
56
|
+
):
|
|
53
57
|
self._set_success = True
|
|
54
|
-
|
|
58
|
+
(
|
|
59
|
+
old_position,
|
|
60
|
+
units,
|
|
61
|
+
precision,
|
|
62
|
+
velocity,
|
|
63
|
+
acceleration_time,
|
|
64
|
+
) = await asyncio.gather(
|
|
55
65
|
self.user_setpoint.get_value(),
|
|
56
66
|
self.motor_egu.get_value(),
|
|
57
67
|
self.precision.get_value(),
|
|
68
|
+
self.velocity.get_value(),
|
|
69
|
+
self.acceleration_time.get_value(),
|
|
58
70
|
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
unit=units,
|
|
68
|
-
precision=precision,
|
|
69
|
-
),
|
|
70
|
-
move_status,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
@WatchableAsyncStatus.wrap
|
|
74
|
-
async def set(self, new_position: float, timeout: float | None = None):
|
|
75
|
-
update, move_status = await self._move(new_position)
|
|
71
|
+
if timeout is CalculateTimeout:
|
|
72
|
+
assert velocity > 0, "Motor has zero velocity"
|
|
73
|
+
timeout = (
|
|
74
|
+
abs(new_position - old_position) / velocity
|
|
75
|
+
+ 2 * acceleration_time
|
|
76
|
+
+ DEFAULT_TIMEOUT
|
|
77
|
+
)
|
|
78
|
+
move_status = self.user_setpoint.set(new_position, wait=True, timeout=timeout)
|
|
76
79
|
async for current_position in observe_value(
|
|
77
80
|
self.user_readback, done_status=move_status
|
|
78
81
|
):
|
|
79
|
-
|
|
80
|
-
raise RuntimeError("Motor was stopped")
|
|
81
|
-
yield replace(
|
|
82
|
-
update,
|
|
83
|
-
name=self.name,
|
|
82
|
+
yield WatcherUpdate(
|
|
84
83
|
current=current_position,
|
|
84
|
+
initial=old_position,
|
|
85
|
+
target=new_position,
|
|
86
|
+
name=self.name,
|
|
87
|
+
unit=units,
|
|
88
|
+
precision=precision,
|
|
85
89
|
)
|
|
90
|
+
if not self._set_success:
|
|
91
|
+
raise RuntimeError("Motor was stopped")
|
|
86
92
|
|
|
87
93
|
async def stop(self, success=False):
|
|
88
94
|
self._set_success = success
|
ophyd_async/panda/__init__.py
CHANGED
|
@@ -15,6 +15,7 @@ from ._table import (
|
|
|
15
15
|
seq_table_from_arrays,
|
|
16
16
|
seq_table_from_rows,
|
|
17
17
|
)
|
|
18
|
+
from ._trigger import StaticSeqTableTriggerLogic
|
|
18
19
|
from ._utils import phase_sorter
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
@@ -33,4 +34,5 @@ __all__ = [
|
|
|
33
34
|
"TimeUnits",
|
|
34
35
|
"DataBlock",
|
|
35
36
|
"CommonPandABlocks",
|
|
37
|
+
"StaticSeqTableTriggerLogic",
|
|
36
38
|
]
|
|
@@ -79,10 +79,10 @@ async def get_signals_marked_for_capture(
|
|
|
79
79
|
capture_signals.keys(), capture_signals.values(), signal_values
|
|
80
80
|
):
|
|
81
81
|
signal_path = signal_path.replace("_capture", "")
|
|
82
|
-
if (signal_value
|
|
82
|
+
if (signal_value in iter(Capture)) and (signal_value != Capture.No):
|
|
83
83
|
signals_to_capture[signal_path] = CaptureSignalWrapper(
|
|
84
84
|
signal_object,
|
|
85
|
-
signal_value
|
|
85
|
+
signal_value,
|
|
86
86
|
)
|
|
87
87
|
|
|
88
88
|
return signals_to_capture
|
|
@@ -126,7 +126,7 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
126
126
|
str(info.root / info.resource_dir)
|
|
127
127
|
),
|
|
128
128
|
self.panda_device.data.hdf_file_name.set(
|
|
129
|
-
f"{info.prefix}{self.panda_device.name}{info.suffix}",
|
|
129
|
+
f"{info.prefix}{self.panda_device.name}{info.suffix}.h5",
|
|
130
130
|
),
|
|
131
131
|
self.panda_device.data.num_capture.set(0),
|
|
132
132
|
)
|
|
@@ -149,7 +149,7 @@ class PandaHDFWriter(DetectorWriter):
|
|
|
149
149
|
else split_path[-2]
|
|
150
150
|
)
|
|
151
151
|
|
|
152
|
-
for suffix in
|
|
152
|
+
for suffix in capture_signal.capture_type.split(" "):
|
|
153
153
|
self._datasets.append(
|
|
154
154
|
_HDFDataset(
|
|
155
155
|
name,
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
from .ensure_connected import ensure_connected
|
|
2
|
-
from .
|
|
2
|
+
from .fly import (
|
|
3
|
+
fly_and_collect,
|
|
3
4
|
prepare_static_seq_table_flyer_and_detectors_with_same_trigger,
|
|
5
|
+
time_resolved_fly_and_collect_with_static_seq_table,
|
|
4
6
|
)
|
|
5
7
|
|
|
6
8
|
__all__ = [
|
|
9
|
+
"fly_and_collect",
|
|
7
10
|
"prepare_static_seq_table_flyer_and_detectors_with_same_trigger",
|
|
11
|
+
"time_resolved_fly_and_collect_with_static_seq_table",
|
|
8
12
|
"ensure_connected",
|
|
9
13
|
]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
import bluesky.plan_stubs as bps
|
|
4
|
+
from bluesky.utils import short_uid
|
|
5
|
+
|
|
6
|
+
from ophyd_async.core.detector import DetectorTrigger, StandardDetector, TriggerInfo
|
|
7
|
+
from ophyd_async.core.flyer import HardwareTriggeredFlyable
|
|
8
|
+
from ophyd_async.core.utils import in_micros
|
|
9
|
+
from ophyd_async.panda._table import SeqTable, SeqTableRow, seq_table_from_rows
|
|
10
|
+
from ophyd_async.panda._trigger import SeqTableInfo
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
14
|
+
flyer: HardwareTriggeredFlyable[SeqTableInfo],
|
|
15
|
+
detectors: List[StandardDetector],
|
|
16
|
+
number_of_frames: int,
|
|
17
|
+
exposure: float,
|
|
18
|
+
shutter_time: float,
|
|
19
|
+
repeats: int = 1,
|
|
20
|
+
period: float = 0.0,
|
|
21
|
+
):
|
|
22
|
+
"""Prepare a hardware triggered flyable and one or more detectors.
|
|
23
|
+
|
|
24
|
+
Prepare a hardware triggered flyable and one or more detectors with the
|
|
25
|
+
same trigger. This method constructs TriggerInfo and a static sequence
|
|
26
|
+
table from required parameters. The table is required to prepare the flyer,
|
|
27
|
+
and the TriggerInfo is required to prepare the detector(s).
|
|
28
|
+
|
|
29
|
+
This prepares all supplied detectors with the same trigger.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
if not detectors:
|
|
33
|
+
raise ValueError("No detectors provided. There must be at least one.")
|
|
34
|
+
|
|
35
|
+
deadtime = max(det.controller.get_deadtime(exposure) for det in detectors)
|
|
36
|
+
|
|
37
|
+
trigger_info = TriggerInfo(
|
|
38
|
+
num=number_of_frames * repeats,
|
|
39
|
+
trigger=DetectorTrigger.constant_gate,
|
|
40
|
+
deadtime=deadtime,
|
|
41
|
+
livetime=exposure,
|
|
42
|
+
)
|
|
43
|
+
trigger_time = number_of_frames * (exposure + deadtime)
|
|
44
|
+
pre_delay = max(period - 2 * shutter_time - trigger_time, 0)
|
|
45
|
+
|
|
46
|
+
table: SeqTable = seq_table_from_rows(
|
|
47
|
+
# Wait for pre-delay then open shutter
|
|
48
|
+
SeqTableRow(
|
|
49
|
+
time1=in_micros(pre_delay),
|
|
50
|
+
time2=in_micros(shutter_time),
|
|
51
|
+
outa2=True,
|
|
52
|
+
),
|
|
53
|
+
# Keeping shutter open, do N triggers
|
|
54
|
+
SeqTableRow(
|
|
55
|
+
repeats=number_of_frames,
|
|
56
|
+
time1=in_micros(exposure),
|
|
57
|
+
outa1=True,
|
|
58
|
+
outb1=True,
|
|
59
|
+
time2=in_micros(deadtime),
|
|
60
|
+
outa2=True,
|
|
61
|
+
),
|
|
62
|
+
# Add the shutter close
|
|
63
|
+
SeqTableRow(time2=in_micros(shutter_time)),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
table_info = SeqTableInfo(table, repeats)
|
|
67
|
+
|
|
68
|
+
for det in detectors:
|
|
69
|
+
yield from bps.prepare(det, trigger_info, wait=False, group="prep")
|
|
70
|
+
yield from bps.prepare(flyer, table_info, wait=False, group="prep")
|
|
71
|
+
yield from bps.wait(group="prep")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def fly_and_collect(
|
|
75
|
+
stream_name: str,
|
|
76
|
+
flyer: HardwareTriggeredFlyable[SeqTableInfo],
|
|
77
|
+
detectors: List[StandardDetector],
|
|
78
|
+
):
|
|
79
|
+
"""Kickoff, complete and collect with a flyer and multiple detectors.
|
|
80
|
+
|
|
81
|
+
This stub takes a flyer and one or more detectors that have been prepared. It
|
|
82
|
+
declares a stream for the detectors, then kicks off the detectors and the flyer.
|
|
83
|
+
The detectors are collected until the flyer and detectors have completed.
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
yield from bps.declare_stream(*detectors, name=stream_name, collect=True)
|
|
87
|
+
yield from bps.kickoff(flyer, wait=True)
|
|
88
|
+
for detector in detectors:
|
|
89
|
+
yield from bps.kickoff(detector)
|
|
90
|
+
|
|
91
|
+
# collect_while_completing
|
|
92
|
+
group = short_uid(label="complete")
|
|
93
|
+
|
|
94
|
+
yield from bps.complete(flyer, wait=False, group=group)
|
|
95
|
+
for detector in detectors:
|
|
96
|
+
yield from bps.complete(detector, wait=False, group=group)
|
|
97
|
+
|
|
98
|
+
done = False
|
|
99
|
+
while not done:
|
|
100
|
+
try:
|
|
101
|
+
yield from bps.wait(group=group, timeout=0.5)
|
|
102
|
+
except TimeoutError:
|
|
103
|
+
pass
|
|
104
|
+
else:
|
|
105
|
+
done = True
|
|
106
|
+
yield from bps.collect(
|
|
107
|
+
*detectors,
|
|
108
|
+
return_payload=False,
|
|
109
|
+
name=stream_name,
|
|
110
|
+
)
|
|
111
|
+
yield from bps.wait(group=group)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def time_resolved_fly_and_collect_with_static_seq_table(
|
|
115
|
+
stream_name: str,
|
|
116
|
+
flyer: HardwareTriggeredFlyable[SeqTableInfo],
|
|
117
|
+
detectors: List[StandardDetector],
|
|
118
|
+
number_of_frames: int,
|
|
119
|
+
exposure: float,
|
|
120
|
+
shutter_time: float,
|
|
121
|
+
repeats: int = 1,
|
|
122
|
+
period: float = 0.0,
|
|
123
|
+
):
|
|
124
|
+
"""Run a scan wth a flyer and multiple detectors.
|
|
125
|
+
|
|
126
|
+
The stub demonstrates the standard basic flow for a flyscan:
|
|
127
|
+
|
|
128
|
+
- Prepare the flyer and detectors with a trigger
|
|
129
|
+
- Fly and collect:
|
|
130
|
+
- Declare the stream and kickoff the scan
|
|
131
|
+
- Collect while completing
|
|
132
|
+
|
|
133
|
+
This needs to be used in a plan that instantates detectors and a flyer,
|
|
134
|
+
stages/unstages the devices, and opens and closes the run.
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
# Set up scan and prepare trigger
|
|
139
|
+
yield from prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
140
|
+
flyer,
|
|
141
|
+
detectors,
|
|
142
|
+
number_of_frames=number_of_frames,
|
|
143
|
+
exposure=exposure,
|
|
144
|
+
shutter_time=shutter_time,
|
|
145
|
+
repeats=repeats,
|
|
146
|
+
period=period,
|
|
147
|
+
)
|
|
148
|
+
# Run the fly scan
|
|
149
|
+
yield from fly_and_collect(stream_name, flyer, detectors)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import contextlib
|
|
2
3
|
import time
|
|
3
|
-
from dataclasses import replace
|
|
4
4
|
|
|
5
5
|
from bluesky.protocols import Movable, Stoppable
|
|
6
6
|
|
|
@@ -25,109 +25,79 @@ class SimMotor(StandardReadable, Movable, Stoppable):
|
|
|
25
25
|
- name: str: name of device
|
|
26
26
|
- instant: bool: whether to move instantly, or with a delay
|
|
27
27
|
"""
|
|
28
|
+
# Define some signals
|
|
28
29
|
with self.add_children_as_readables(HintedSignal):
|
|
29
30
|
self.user_readback, self._user_readback_set = soft_signal_r_and_setter(
|
|
30
31
|
float, 0
|
|
31
32
|
)
|
|
32
|
-
|
|
33
33
|
with self.add_children_as_readables(ConfigSignal):
|
|
34
|
-
self.velocity = soft_signal_rw(float, 1.0)
|
|
35
|
-
self.
|
|
36
|
-
|
|
37
|
-
self._instant = instant
|
|
38
|
-
self._move_status: AsyncStatus | None = None
|
|
39
|
-
|
|
40
|
-
# Define some signals
|
|
34
|
+
self.velocity = soft_signal_rw(float, 0 if instant else 1.0)
|
|
35
|
+
self.units = soft_signal_rw(str, "mm")
|
|
41
36
|
self.user_setpoint = soft_signal_rw(float, 0)
|
|
42
37
|
|
|
43
|
-
super().__init__(name=name)
|
|
44
|
-
|
|
45
38
|
# Whether set() should complete successfully or not
|
|
46
39
|
self._set_success = True
|
|
40
|
+
self._move_status: AsyncStatus | None = None
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
"""
|
|
50
|
-
Stop the motor if it is moving
|
|
51
|
-
"""
|
|
52
|
-
if self._move_status:
|
|
53
|
-
self._move_status.task.cancel()
|
|
54
|
-
self._move_status = None
|
|
42
|
+
super().__init__(name=name)
|
|
55
43
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
async def _move(self, old_position: float, new_position: float, move_time: float):
|
|
45
|
+
start = time.monotonic()
|
|
46
|
+
distance = abs(new_position - old_position)
|
|
47
|
+
while True:
|
|
48
|
+
time_elapsed = round(time.monotonic() - start, 2)
|
|
60
49
|
|
|
61
|
-
|
|
50
|
+
# update position based on time elapsed
|
|
51
|
+
if time_elapsed >= move_time:
|
|
52
|
+
# successfully reached our target position
|
|
53
|
+
self._user_readback_set(new_position)
|
|
54
|
+
break
|
|
55
|
+
else:
|
|
56
|
+
current_position = old_position + distance * time_elapsed / move_time
|
|
62
57
|
|
|
63
|
-
|
|
58
|
+
self._user_readback_set(current_position)
|
|
59
|
+
|
|
60
|
+
# 10hz update loop
|
|
61
|
+
await asyncio.sleep(0.1)
|
|
64
62
|
|
|
65
63
|
@WatchableAsyncStatus.wrap
|
|
66
|
-
async def set(self, new_position: float
|
|
64
|
+
async def set(self, new_position: float):
|
|
67
65
|
"""
|
|
68
66
|
Asynchronously move the motor to a new position.
|
|
69
67
|
"""
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
):
|
|
74
|
-
if not self._set_success:
|
|
75
|
-
raise RuntimeError("Motor was stopped")
|
|
76
|
-
yield replace(
|
|
77
|
-
update,
|
|
78
|
-
name=self.name,
|
|
79
|
-
current=current_position,
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
async def _move(self, new_position: float, timeout: float | None = None):
|
|
83
|
-
"""
|
|
84
|
-
Start the motor moving to a new position.
|
|
85
|
-
|
|
86
|
-
If the motor is already moving, it will stop first.
|
|
87
|
-
If this is an instant motor the move will be instantaneous.
|
|
88
|
-
"""
|
|
89
|
-
self.stop()
|
|
90
|
-
start = time.monotonic()
|
|
91
|
-
self._set_success = True
|
|
92
|
-
|
|
93
|
-
current_position = await self.user_readback.get_value()
|
|
94
|
-
distance = abs(new_position - current_position)
|
|
95
|
-
travel_time = 0 if self._instant else distance / await self.velocity.get_value()
|
|
96
|
-
|
|
97
|
-
old_position, units = await asyncio.gather(
|
|
68
|
+
# Make sure any existing move tasks are stopped
|
|
69
|
+
await self.stop()
|
|
70
|
+
old_position, units, velocity = await asyncio.gather(
|
|
98
71
|
self.user_setpoint.get_value(),
|
|
99
|
-
self.
|
|
72
|
+
self.units.get_value(),
|
|
73
|
+
self.velocity.get_value(),
|
|
100
74
|
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# update position based on time elapsed
|
|
107
|
-
if time_elapsed >= travel_time:
|
|
108
|
-
# successfully reached our target position
|
|
109
|
-
self._user_readback_set(new_position)
|
|
110
|
-
self._set_success = True
|
|
111
|
-
break
|
|
112
|
-
else:
|
|
113
|
-
current_position = (
|
|
114
|
-
old_position + distance * time_elapsed / travel_time
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
self._user_readback_set(current_position)
|
|
118
|
-
|
|
119
|
-
# 10hz update loop
|
|
120
|
-
await asyncio.sleep(0.1)
|
|
121
|
-
|
|
122
|
-
# set up a task that updates the motor position at ~10hz
|
|
123
|
-
self._move_status = AsyncStatus(asyncio.wait_for(update_position(), timeout))
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
WatcherUpdate(
|
|
127
|
-
initial=old_position,
|
|
128
|
-
current=old_position,
|
|
129
|
-
target=new_position,
|
|
130
|
-
unit=units,
|
|
131
|
-
),
|
|
132
|
-
self._move_status,
|
|
75
|
+
# If zero velocity, do instant move
|
|
76
|
+
move_time = abs(new_position - old_position) / velocity if velocity else 0
|
|
77
|
+
self._move_status = AsyncStatus(
|
|
78
|
+
self._move(old_position, new_position, move_time)
|
|
133
79
|
)
|
|
80
|
+
# If stop is called then this will raise a CancelledError, ignore it
|
|
81
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
82
|
+
async for current_position in observe_value(
|
|
83
|
+
self.user_readback, done_status=self._move_status
|
|
84
|
+
):
|
|
85
|
+
yield WatcherUpdate(
|
|
86
|
+
current=current_position,
|
|
87
|
+
initial=old_position,
|
|
88
|
+
target=new_position,
|
|
89
|
+
name=self.name,
|
|
90
|
+
unit=units,
|
|
91
|
+
)
|
|
92
|
+
if not self._set_success:
|
|
93
|
+
raise RuntimeError("Motor was stopped")
|
|
94
|
+
|
|
95
|
+
async def stop(self, success=True):
|
|
96
|
+
"""
|
|
97
|
+
Stop the motor if it is moving
|
|
98
|
+
"""
|
|
99
|
+
self._set_success = success
|
|
100
|
+
if self._move_status:
|
|
101
|
+
self._move_status.task.cancel()
|
|
102
|
+
self._move_status = None
|
|
103
|
+
await self.user_setpoint.set(await self.user_readback.get_value())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ophyd-async
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3a6
|
|
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,22 +1,22 @@
|
|
|
1
1
|
ophyd_async/__init__.py,sha256=v-rRiDOgZ3sQSMQKq0vgUQZvpeOkoHFXissAx6Ktg84,61
|
|
2
2
|
ophyd_async/__main__.py,sha256=G-Zcv_G9zK7Nhx6o5L5w-wyhMxdl_WgyMELu8IMFqAE,328
|
|
3
|
-
ophyd_async/_version.py,sha256=
|
|
3
|
+
ophyd_async/_version.py,sha256=LuF-BqHYcrns31aIHwEtbxWREi37LwJcIxVOaUpUzgM,408
|
|
4
4
|
ophyd_async/log.py,sha256=DbMjt0bkfUOLHIinZYt0Q0FHZmCXXi5x8y0uFiEmqoQ,3587
|
|
5
5
|
ophyd_async/protocols.py,sha256=EF2W9nfElV-0QNMYrX1zusL1PqDJR3kNsjlalR29j0I,3412
|
|
6
|
-
ophyd_async/core/__init__.py,sha256=
|
|
6
|
+
ophyd_async/core/__init__.py,sha256=wF90GU1BHjCzVVgBtUDHoogCIx6XNXq050wth9YPzVo,2991
|
|
7
7
|
ophyd_async/core/_providers.py,sha256=LrlTMPHKXWOPVkpAOw-pqBq0kip-c3C9ZZPoFfiaV4M,2212
|
|
8
|
-
ophyd_async/core/async_status.py,sha256=
|
|
9
|
-
ophyd_async/core/detector.py,sha256=
|
|
10
|
-
ophyd_async/core/device.py,sha256=
|
|
8
|
+
ophyd_async/core/async_status.py,sha256=9TOgOXIAuH62RDo5t-Y5GdjrJ76d_6TFlBxYv-5_a88,4367
|
|
9
|
+
ophyd_async/core/detector.py,sha256=8mdLKphirgit5CVCklJI9eHqKKiCz4CYs9BElo10-lc,11007
|
|
10
|
+
ophyd_async/core/device.py,sha256=280zFnLCoiMZAA-Dh1_AjUSnhxUfKYGgj4H_2S1njOA,7086
|
|
11
11
|
ophyd_async/core/device_save_loader.py,sha256=RXA3dPUPihAR2ZGDStlGiA-TAsr_xqL0snsCjMsMnfA,9138
|
|
12
|
-
ophyd_async/core/flyer.py,sha256=
|
|
12
|
+
ophyd_async/core/flyer.py,sha256=bIjzBkrl8HVAlKgsZ_FF0WL69Qvksyzp9ZWmTLl8Yrw,2304
|
|
13
13
|
ophyd_async/core/mock_signal_backend.py,sha256=13BAbQG1OPC3o_fOFFQH4x1mPYLQ8Zf53WviRPXWDTM,2938
|
|
14
|
-
ophyd_async/core/mock_signal_utils.py,sha256=
|
|
15
|
-
ophyd_async/core/signal.py,sha256=
|
|
14
|
+
ophyd_async/core/mock_signal_utils.py,sha256=TgVV3D6i7ItXRiy98H1Ov8dKZhiop_9CdTaRjiH5cwQ,4143
|
|
15
|
+
ophyd_async/core/signal.py,sha256=FbTb5qDPLhVxEbh6gimqXfkZwcqB4ymHTEYVXZVZYrk,16456
|
|
16
16
|
ophyd_async/core/signal_backend.py,sha256=qDdWz8X4CWStuYknxcj4G76BLq4TzrAIyZO1NOEq9ao,1519
|
|
17
17
|
ophyd_async/core/soft_signal_backend.py,sha256=56zvcEi4c8n1yYbafTbp7X0VhSkhoehm3L8RBhu2fik,5596
|
|
18
18
|
ophyd_async/core/standard_readable.py,sha256=uVG3vs3s7-Kzg5dRCtT4I2mhZPqwVGYy2dxNmaOpDVU,8980
|
|
19
|
-
ophyd_async/core/utils.py,sha256=
|
|
19
|
+
ophyd_async/core/utils.py,sha256=3oZcXNqAUHX4ZWMBH5gSuK6cFWEhSkZ9GSDYv0pf8jc,5783
|
|
20
20
|
ophyd_async/epics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
ophyd_async/epics/_backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
ophyd_async/epics/_backend/_aioca.py,sha256=cpPNZmRMi7FnAh2-3ec5uklLVFOqsmEmpI1nh5Ud1Ls,8794
|
|
@@ -36,7 +36,7 @@ ophyd_async/epics/areadetector/controllers/kinetix_controller.py,sha256=9QmydX85
|
|
|
36
36
|
ophyd_async/epics/areadetector/controllers/pilatus_controller.py,sha256=cd1CKkaXlwkpQ0I1VL7nN0U8R4VweTsa08WhvHYI4nY,2243
|
|
37
37
|
ophyd_async/epics/areadetector/controllers/vimba_controller.py,sha256=Eh4Hr9rWgq1mKvE93JzgixntjPHxF3_07GTFqiOdZqE,2123
|
|
38
38
|
ophyd_async/epics/areadetector/drivers/__init__.py,sha256=-Ib0Lz4fFQQmB7K0uFxMDvAerkLxadMQERH7lNAvrs4,495
|
|
39
|
-
ophyd_async/epics/areadetector/drivers/ad_base.py,sha256=
|
|
39
|
+
ophyd_async/epics/areadetector/drivers/ad_base.py,sha256=18WFAiWEUg0H2LcvTQHrKYj2wThGafQzDpiyAWki6vo,3411
|
|
40
40
|
ophyd_async/epics/areadetector/drivers/aravis_driver.py,sha256=PmIygsVNoxxYHvZZzFAbAm2DXmXFc13nAzL_DJB6YSU,1464
|
|
41
41
|
ophyd_async/epics/areadetector/drivers/kinetix_driver.py,sha256=yIV23BkGBJ4i0VskLiLL7AFbadCCR6Ch1UwUDJ9r2YM,743
|
|
42
42
|
ophyd_async/epics/areadetector/drivers/pilatus_driver.py,sha256=0DsUu9vAPXDa2v8_V0f_kPjBtLu3y4_EkmFfFjYO4Gk,553
|
|
@@ -44,21 +44,21 @@ ophyd_async/epics/areadetector/drivers/vimba_driver.py,sha256=J54VtWkOklfbSqZYxG
|
|
|
44
44
|
ophyd_async/epics/areadetector/writers/__init__.py,sha256=tpPcrYd1hs8WS7C0gmCnR2EBwjE5RzCljI7WwZ2V_LM,191
|
|
45
45
|
ophyd_async/epics/areadetector/writers/_hdfdataset.py,sha256=E0C9VgsPyY35h7k0mvcIhjsIVNavApLxizqNWlM388w,167
|
|
46
46
|
ophyd_async/epics/areadetector/writers/_hdffile.py,sha256=YtUgOKX53m0TaFEGBW671qXqNuuEKxEyLV5Ein1fjvo,1799
|
|
47
|
-
ophyd_async/epics/areadetector/writers/hdf_writer.py,sha256=
|
|
48
|
-
ophyd_async/epics/areadetector/writers/nd_file_hdf.py,sha256=
|
|
49
|
-
ophyd_async/epics/areadetector/writers/nd_plugin.py,sha256=
|
|
50
|
-
ophyd_async/epics/demo/__init__.py,sha256=
|
|
47
|
+
ophyd_async/epics/areadetector/writers/hdf_writer.py,sha256=ZpbVilNVv81OpbCrqaeZUoHLarrjzRWEGe-zI1Wxyyw,5436
|
|
48
|
+
ophyd_async/epics/areadetector/writers/nd_file_hdf.py,sha256=EkiaEh_0U6Iz17jFi2IIsRPsVQTQIJRG8EPNCiAHkkU,1762
|
|
49
|
+
ophyd_async/epics/areadetector/writers/nd_plugin.py,sha256=GUzaeTMdG07Rb1x0WzBBxMEltBhr10jb5dL29tEWXEQ,1547
|
|
50
|
+
ophyd_async/epics/demo/__init__.py,sha256=ZcuZ66aIQ58WSydLOSKnk-h_W-aWjhDRZkWQA6f3sig,5790
|
|
51
51
|
ophyd_async/epics/demo/demo_ad_sim_detector.py,sha256=06y65yvaqXvL2rDocjYyLz9kTVzuwV-LeuPhEfExdOA,944
|
|
52
52
|
ophyd_async/epics/demo/mover.db,sha256=RFz0rxZue689Wh1sWTZwWeFMUrH04ttPq2u5xJH_Fp4,998
|
|
53
53
|
ophyd_async/epics/demo/sensor.db,sha256=AVtiydrdtwAz2EFurO2Ult9SSRtre3r0akOBbL98LT0,554
|
|
54
54
|
ophyd_async/epics/motion/__init__.py,sha256=tnmVRIwKa9PdN_xonJdAUD04UpEceh-hoD7XI62yDB0,46
|
|
55
|
-
ophyd_async/epics/motion/motor.py,sha256=
|
|
55
|
+
ophyd_async/epics/motion/motor.py,sha256=G8cc-okSXJ6s2fGxRO155xm7PrBbVImBmBMRWts895k,3630
|
|
56
56
|
ophyd_async/epics/pvi/__init__.py,sha256=TbOQNY4enQWgtr1T7x129vpo2p7FIFlr8cyZqqv5Lk4,158
|
|
57
57
|
ophyd_async/epics/pvi/pvi.py,sha256=PJdY3rCRyIQbsbHDru-TJ-IVOItyaQwCQKAC0Widu6A,11363
|
|
58
58
|
ophyd_async/epics/signal/__init__.py,sha256=JXKBSGpRL9y3auh27JRxsqDn_rBOXpJjtd4nCuDOX2g,261
|
|
59
59
|
ophyd_async/epics/signal/_epics_transport.py,sha256=DEIL0iYUAWssysVEgWGu1fHSM1l-ATV2kjUgPtDN9LY,858
|
|
60
60
|
ophyd_async/epics/signal/signal.py,sha256=M8ZVG_zLdYJfroCRX-u_w8c3yIhswSRw8e3RkW2szio,3166
|
|
61
|
-
ophyd_async/panda/__init__.py,sha256=
|
|
61
|
+
ophyd_async/panda/__init__.py,sha256=FuSnvp-RtdA0X4RcHEF0nTiXymRts2MNdFmF_1_i41w,775
|
|
62
62
|
ophyd_async/panda/_common_blocks.py,sha256=n0PPc1rar43oDSIA-yNubTc8fR5YCW1tyjQU58whsg0,1038
|
|
63
63
|
ophyd_async/panda/_hdf_panda.py,sha256=QjfZyYos0ZBlIqBiZ5UbyEd_wuh_cGzwV8QE9jvLiIY,1419
|
|
64
64
|
ophyd_async/panda/_panda_controller.py,sha256=dIqcjmaIHVrki8UXSoDx46kk6I2Lhpe2o3sXNg5f-RQ,1238
|
|
@@ -66,21 +66,21 @@ ophyd_async/panda/_table.py,sha256=dLoRP4zYNOkD_s0Vkp2wVYAwkjVG8nNdf8-FaXOTfPo,5
|
|
|
66
66
|
ophyd_async/panda/_trigger.py,sha256=tBH8uq_4o1ASG9yofVxq3tjf5v8LPzniDTRL4yjramI,1195
|
|
67
67
|
ophyd_async/panda/_utils.py,sha256=VHW5kPVISyEkmse_qQcyisBkkEwMO6GG2Ago-CH1AFA,487
|
|
68
68
|
ophyd_async/panda/writers/__init__.py,sha256=xy7BguVQG4HNIDBfKPjMj0KQo1tptC9LbCpEuMcVGaM,70
|
|
69
|
-
ophyd_async/panda/writers/_hdf_writer.py,sha256=
|
|
69
|
+
ophyd_async/panda/writers/_hdf_writer.py,sha256=vnyIg3JmlzMIIq75o0IDMfGzBm_GJAhOUisAZE_0cyg,7597
|
|
70
70
|
ophyd_async/panda/writers/_panda_hdf_file.py,sha256=42iHaTax4JjOBpNC7d4nkNL9SM14OTnFPTIcXv2jg-4,1759
|
|
71
|
-
ophyd_async/
|
|
72
|
-
ophyd_async/
|
|
73
|
-
ophyd_async/
|
|
71
|
+
ophyd_async/plan_stubs/__init__.py,sha256=nO9ELG9J7fYwfVTVRWVorz4kffeszYpwk1ROh6Ha--w,405
|
|
72
|
+
ophyd_async/plan_stubs/ensure_connected.py,sha256=1MkDu8UqVRPHLnW9IXRn-QvKiG8-rCV8T4KDbjf9K6w,557
|
|
73
|
+
ophyd_async/plan_stubs/fly.py,sha256=nl8XLoY7hvlam6H3zl4NcPRUiEJ3xIjopHEfA0ehTDg,4845
|
|
74
74
|
ophyd_async/sim/__init__.py,sha256=ScjH1g7FMo5yPACfJRZE6xGBWCHU4bKDzNQk1tqObnA,366
|
|
75
75
|
ophyd_async/sim/pattern_generator.py,sha256=pvSk2zb82D08j2jiKAMqMAfRohGnYd_rpjUraLrCD6c,10640
|
|
76
76
|
ophyd_async/sim/sim_pattern_detector_control.py,sha256=Ypz8IuRYAY2J243IhVbNyGr_Z-XtpJZ1qxma6NR3TgM,1838
|
|
77
77
|
ophyd_async/sim/sim_pattern_detector_writer.py,sha256=ESpcVyHd1TP7Cojznv2hJAwLinu3XbgAiVKfX12FCII,1237
|
|
78
78
|
ophyd_async/sim/sim_pattern_generator.py,sha256=fbcwWxTPYKLK33OzIY15vGylnonOO8HIudz1y_56GZU,1336
|
|
79
79
|
ophyd_async/sim/demo/__init__.py,sha256=9mxKpslrL89cfSj4g3og8Br3O--pMj3hhWZS-Xu6kyA,56
|
|
80
|
-
ophyd_async/sim/demo/sim_motor.py,sha256=
|
|
81
|
-
ophyd_async-0.
|
|
82
|
-
ophyd_async-0.
|
|
83
|
-
ophyd_async-0.
|
|
84
|
-
ophyd_async-0.
|
|
85
|
-
ophyd_async-0.
|
|
86
|
-
ophyd_async-0.
|
|
80
|
+
ophyd_async/sim/demo/sim_motor.py,sha256=a2p5wnHXjF-V5zOFai7jnszk4kbGmrZRnUqBtkOgEfQ,3733
|
|
81
|
+
ophyd_async-0.3a6.dist-info/LICENSE,sha256=pU5shZcsvWgz701EbT7yjFZ8rMvZcWgRH54CRt8ld_c,1517
|
|
82
|
+
ophyd_async-0.3a6.dist-info/METADATA,sha256=oFejS6nfk0zaWbJASTeSieIQnPMjC1mvkIFaUrmH1MQ,6284
|
|
83
|
+
ophyd_async-0.3a6.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
84
|
+
ophyd_async-0.3a6.dist-info/entry_points.txt,sha256=O0YNJTEufO0w9BozXi-JurTy2U1_o0ypeCgJLQ727Jk,58
|
|
85
|
+
ophyd_async-0.3a6.dist-info/top_level.txt,sha256=-hjorMsv5Rmjo3qrgqhjpal1N6kW5vMxZO3lD4iEaXs,12
|
|
86
|
+
ophyd_async-0.3a6.dist-info/RECORD,,
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
from typing import List
|
|
2
|
-
|
|
3
|
-
import bluesky.plan_stubs as bps
|
|
4
|
-
|
|
5
|
-
from ophyd_async.core.detector import DetectorTrigger, StandardDetector, TriggerInfo
|
|
6
|
-
from ophyd_async.core.flyer import HardwareTriggeredFlyable
|
|
7
|
-
from ophyd_async.core.utils import in_micros
|
|
8
|
-
from ophyd_async.panda._table import SeqTable, SeqTableRow, seq_table_from_rows
|
|
9
|
-
from ophyd_async.panda._trigger import SeqTableInfo
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
13
|
-
flyer: HardwareTriggeredFlyable[SeqTableInfo],
|
|
14
|
-
detectors: List[StandardDetector],
|
|
15
|
-
num: int,
|
|
16
|
-
width: float,
|
|
17
|
-
deadtime: float,
|
|
18
|
-
shutter_time: float,
|
|
19
|
-
repeats: int = 1,
|
|
20
|
-
period: float = 0.0,
|
|
21
|
-
):
|
|
22
|
-
trigger_info = TriggerInfo(
|
|
23
|
-
num=num * repeats,
|
|
24
|
-
trigger=DetectorTrigger.constant_gate,
|
|
25
|
-
deadtime=deadtime,
|
|
26
|
-
livetime=width,
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
trigger_time = num * (width + deadtime)
|
|
30
|
-
pre_delay = max(period - 2 * shutter_time - trigger_time, 0)
|
|
31
|
-
|
|
32
|
-
table: SeqTable = seq_table_from_rows(
|
|
33
|
-
# Wait for pre-delay then open shutter
|
|
34
|
-
SeqTableRow(
|
|
35
|
-
time1=in_micros(pre_delay),
|
|
36
|
-
time2=in_micros(shutter_time),
|
|
37
|
-
outa2=True,
|
|
38
|
-
),
|
|
39
|
-
# Keeping shutter open, do N triggers
|
|
40
|
-
SeqTableRow(
|
|
41
|
-
repeats=num,
|
|
42
|
-
time1=in_micros(width),
|
|
43
|
-
outa1=True,
|
|
44
|
-
outb1=True,
|
|
45
|
-
time2=in_micros(deadtime),
|
|
46
|
-
outa2=True,
|
|
47
|
-
),
|
|
48
|
-
# Add the shutter close
|
|
49
|
-
SeqTableRow(time2=in_micros(shutter_time)),
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
table_info = SeqTableInfo(table, repeats)
|
|
53
|
-
|
|
54
|
-
for det in detectors:
|
|
55
|
-
yield from bps.prepare(det, trigger_info, wait=False, group="prep")
|
|
56
|
-
yield from bps.prepare(flyer, table_info, wait=False, group="prep")
|
|
57
|
-
yield from bps.wait(group="prep")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|