ophyd-async 0.8.0a5__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +17 -46
- ophyd_async/core/_detector.py +68 -44
- ophyd_async/core/_device.py +120 -79
- ophyd_async/core/_device_filler.py +17 -8
- ophyd_async/core/_flyer.py +2 -2
- ophyd_async/core/_protocol.py +0 -28
- ophyd_async/core/_readable.py +30 -23
- ophyd_async/core/_settings.py +104 -0
- ophyd_async/core/_signal.py +164 -151
- ophyd_async/core/_signal_backend.py +4 -1
- ophyd_async/core/_soft_signal_backend.py +2 -1
- ophyd_async/core/_table.py +27 -14
- ophyd_async/core/_utils.py +30 -5
- ophyd_async/core/_yaml_settings.py +64 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +49 -0
- ophyd_async/epics/adandor/_andor_io.py +36 -0
- ophyd_async/epics/adaravis/__init__.py +3 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -37
- ophyd_async/epics/adaravis/_aravis_controller.py +21 -30
- ophyd_async/epics/adaravis/_aravis_io.py +4 -4
- ophyd_async/epics/adcore/__init__.py +15 -8
- ophyd_async/epics/adcore/_core_detector.py +41 -0
- ophyd_async/epics/adcore/_core_io.py +56 -31
- ophyd_async/epics/adcore/_core_logic.py +99 -84
- ophyd_async/epics/adcore/_core_writer.py +219 -0
- ophyd_async/epics/adcore/_hdf_writer.py +33 -59
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +5 -4
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +37 -36
- ophyd_async/epics/adkinetix/_kinetix.py +29 -24
- ophyd_async/epics/adkinetix/_kinetix_controller.py +15 -27
- ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
- ophyd_async/epics/adpilatus/__init__.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +47 -25
- ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
- ophyd_async/epics/adsimdetector/__init__.py +3 -3
- ophyd_async/epics/adsimdetector/_sim.py +33 -17
- ophyd_async/epics/advimba/_vimba.py +23 -23
- ophyd_async/epics/advimba/_vimba_controller.py +21 -35
- ophyd_async/epics/advimba/_vimba_io.py +23 -23
- ophyd_async/epics/core/_aioca.py +52 -21
- ophyd_async/epics/core/_p4p.py +59 -16
- ophyd_async/epics/core/_pvi_connector.py +4 -2
- ophyd_async/epics/core/_signal.py +9 -2
- ophyd_async/epics/core/_util.py +10 -1
- ophyd_async/epics/eiger/_eiger_controller.py +10 -5
- ophyd_async/epics/eiger/_eiger_io.py +3 -3
- ophyd_async/epics/motor.py +26 -15
- ophyd_async/epics/sim/_ioc.py +29 -0
- ophyd_async/epics/{demo → sim}/_mover.py +12 -6
- ophyd_async/epics/{demo → sim}/_sensor.py +2 -2
- ophyd_async/epics/testing/__init__.py +24 -0
- ophyd_async/epics/testing/_example_ioc.py +91 -0
- ophyd_async/epics/testing/_utils.py +50 -0
- ophyd_async/epics/testing/test_records.db +174 -0
- ophyd_async/epics/testing/test_records_pva.db +177 -0
- ophyd_async/fastcs/core.py +2 -2
- ophyd_async/fastcs/panda/__init__.py +0 -2
- ophyd_async/fastcs/panda/_block.py +9 -9
- ophyd_async/fastcs/panda/_control.py +9 -4
- ophyd_async/fastcs/panda/_hdf_panda.py +7 -2
- ophyd_async/fastcs/panda/_table.py +4 -1
- ophyd_async/fastcs/panda/_trigger.py +7 -7
- ophyd_async/plan_stubs/__init__.py +14 -0
- ophyd_async/plan_stubs/_ensure_connected.py +11 -17
- ophyd_async/plan_stubs/_fly.py +2 -2
- ophyd_async/plan_stubs/_nd_attributes.py +7 -5
- ophyd_async/plan_stubs/_panda.py +13 -0
- ophyd_async/plan_stubs/_settings.py +125 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +19 -0
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
- ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +34 -32
- ophyd_async/tango/__init__.py +0 -43
- ophyd_async/tango/{signal → core}/__init__.py +7 -2
- ophyd_async/tango/{base_devices → core}/_base_device.py +38 -64
- ophyd_async/tango/{signal → core}/_signal.py +16 -4
- ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
- ophyd_async/tango/{signal → core}/_tango_transport.py +13 -15
- ophyd_async/tango/{demo → sim}/_counter.py +6 -7
- ophyd_async/tango/{demo → sim}/_mover.py +13 -9
- ophyd_async/testing/__init__.py +52 -0
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +176 -0
- ophyd_async/{core → testing}/_mock_signal_utils.py +15 -11
- ophyd_async/testing/_one_of_everything.py +126 -0
- ophyd_async/testing/_wait_for_pending.py +22 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/METADATA +50 -48
- ophyd_async-0.9.0.dist-info/RECORD +129 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async/tango/base_devices/__init__.py +0 -4
- ophyd_async-0.8.0a5.dist-info/RECORD +0 -112
- ophyd_async-0.8.0a5.dist-info/entry_points.txt +0 -2
- /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
- /ophyd_async/epics/{demo → sim}/mover.db +0 -0
- /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
- /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0.dist-info}/top_level.txt +0 -0
ophyd_async/core/_signal.py
CHANGED
|
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import functools
|
|
5
|
-
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
6
7
|
from typing import Any, Generic, cast
|
|
7
8
|
|
|
8
9
|
from bluesky.protocols import (
|
|
@@ -17,7 +18,6 @@ from event_model import DataKey
|
|
|
17
18
|
from ._device import Device, DeviceConnector
|
|
18
19
|
from ._mock_signal_backend import MockSignalBackend
|
|
19
20
|
from ._protocol import (
|
|
20
|
-
AsyncConfigurable,
|
|
21
21
|
AsyncReadable,
|
|
22
22
|
AsyncStageable,
|
|
23
23
|
Reading,
|
|
@@ -28,7 +28,7 @@ from ._signal_backend import (
|
|
|
28
28
|
SignalDatatypeV,
|
|
29
29
|
)
|
|
30
30
|
from ._soft_signal_backend import SoftSignalBackend
|
|
31
|
-
from ._status import AsyncStatus
|
|
31
|
+
from ._status import AsyncStatus, completed_status
|
|
32
32
|
from ._utils import (
|
|
33
33
|
CALCULATE_TIMEOUT,
|
|
34
34
|
DEFAULT_TIMEOUT,
|
|
@@ -97,32 +97,37 @@ class Signal(Device, Generic[SignalDatatypeT]):
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
class _SignalCache(Generic[SignalDatatypeT]):
|
|
100
|
-
def __init__(self, backend: SignalBackend[SignalDatatypeT], signal: Signal):
|
|
101
|
-
self._signal = signal
|
|
100
|
+
def __init__(self, backend: SignalBackend[SignalDatatypeT], signal: Signal) -> None:
|
|
101
|
+
self._signal: Signal[Any] = signal
|
|
102
102
|
self._staged = False
|
|
103
103
|
self._listeners: dict[Callback, bool] = {}
|
|
104
104
|
self._valid = asyncio.Event()
|
|
105
105
|
self._reading: Reading[SignalDatatypeT] | None = None
|
|
106
|
-
self.backend = backend
|
|
106
|
+
self.backend: SignalBackend[SignalDatatypeT] = backend
|
|
107
107
|
signal.log.debug(f"Making subscription on source {signal.source}")
|
|
108
108
|
backend.set_callback(self._callback)
|
|
109
109
|
|
|
110
|
-
def close(self):
|
|
110
|
+
def close(self) -> None:
|
|
111
111
|
self.backend.set_callback(None)
|
|
112
112
|
self._signal.log.debug(f"Closing subscription on source {self._signal.source}")
|
|
113
113
|
|
|
114
|
+
def _ensure_reading(self) -> Reading[SignalDatatypeT]:
|
|
115
|
+
if not self._reading:
|
|
116
|
+
msg = "Monitor not working"
|
|
117
|
+
raise RuntimeError(msg)
|
|
118
|
+
return self._reading
|
|
119
|
+
|
|
114
120
|
async def get_reading(self) -> Reading[SignalDatatypeT]:
|
|
115
121
|
await self._valid.wait()
|
|
116
|
-
|
|
117
|
-
return self._reading
|
|
122
|
+
return self._ensure_reading()
|
|
118
123
|
|
|
119
124
|
async def get_value(self) -> SignalDatatypeT:
|
|
120
|
-
reading = await self.get_reading()
|
|
125
|
+
reading: Reading[SignalDatatypeT] = await self.get_reading()
|
|
121
126
|
return reading["value"]
|
|
122
127
|
|
|
123
|
-
def _callback(self, reading: Reading[SignalDatatypeT]):
|
|
128
|
+
def _callback(self, reading: Reading[SignalDatatypeT]) -> None:
|
|
124
129
|
self._signal.log.debug(
|
|
125
|
-
f"Updated subscription: reading of source {self._signal.source} changed"
|
|
130
|
+
f"Updated subscription: reading of source {self._signal.source} changed "
|
|
126
131
|
f"from {self._reading} to {reading}"
|
|
127
132
|
)
|
|
128
133
|
self._reading = reading
|
|
@@ -134,12 +139,10 @@ class _SignalCache(Generic[SignalDatatypeT]):
|
|
|
134
139
|
self,
|
|
135
140
|
function: Callback[dict[str, Reading[SignalDatatypeT]] | SignalDatatypeT],
|
|
136
141
|
want_value: bool,
|
|
137
|
-
):
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
else:
|
|
142
|
-
function({self._signal.name: self._reading})
|
|
142
|
+
) -> None:
|
|
143
|
+
function(self._ensure_reading()["value"]) if want_value else function(
|
|
144
|
+
{self._signal.name: self._ensure_reading()}
|
|
145
|
+
)
|
|
143
146
|
|
|
144
147
|
def subscribe(self, function: Callback, want_value: bool) -> None:
|
|
145
148
|
self._listeners[function] = want_value
|
|
@@ -150,7 +153,7 @@ class _SignalCache(Generic[SignalDatatypeT]):
|
|
|
150
153
|
self._listeners.pop(function)
|
|
151
154
|
return self._staged or bool(self._listeners)
|
|
152
155
|
|
|
153
|
-
def set_staged(self, staged: bool):
|
|
156
|
+
def set_staged(self, staged: bool) -> bool:
|
|
154
157
|
self._staged = staged
|
|
155
158
|
return self._staged or bool(self._listeners)
|
|
156
159
|
|
|
@@ -167,7 +170,10 @@ class SignalR(Signal[SignalDatatypeT], AsyncReadable, AsyncStageable, Subscribab
|
|
|
167
170
|
if cached is None:
|
|
168
171
|
cached = self._cache is not None
|
|
169
172
|
if cached:
|
|
170
|
-
|
|
173
|
+
if not self._cache:
|
|
174
|
+
msg = f"{self.source} not being monitored"
|
|
175
|
+
raise RuntimeError(msg)
|
|
176
|
+
# assert self._cache, f"{self.source} not being monitored"
|
|
171
177
|
return self._cache
|
|
172
178
|
else:
|
|
173
179
|
return self._connector.backend
|
|
@@ -301,137 +307,70 @@ def soft_signal_r_and_setter(
|
|
|
301
307
|
return (signal, backend.set_value)
|
|
302
308
|
|
|
303
309
|
|
|
304
|
-
def
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
+ f"\nbut actually got \n{FAIL}{actual_result}{ENDC}"
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
|
|
316
|
-
"""Assert a signal's value and compare it an expected signal.
|
|
310
|
+
async def observe_value(
|
|
311
|
+
signal: SignalR[SignalDatatypeT],
|
|
312
|
+
timeout: float | None = None,
|
|
313
|
+
done_status: Status | None = None,
|
|
314
|
+
done_timeout: float | None = None,
|
|
315
|
+
) -> AsyncGenerator[SignalDatatypeT, None]:
|
|
316
|
+
"""Subscribe to the value of a signal so it can be iterated from.
|
|
317
317
|
|
|
318
318
|
Parameters
|
|
319
319
|
----------
|
|
320
320
|
signal:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
321
|
+
Call subscribe_value on this at the start, and clear_sub on it at the
|
|
322
|
+
end
|
|
323
|
+
timeout:
|
|
324
|
+
If given, how long to wait for each updated value in seconds. If an update
|
|
325
|
+
is not produced in this time then raise asyncio.TimeoutError
|
|
326
|
+
done_status:
|
|
327
|
+
If this status is complete, stop observing and make the iterator return.
|
|
328
|
+
If it raises an exception then this exception will be raised by the iterator.
|
|
329
|
+
done_timeout:
|
|
330
|
+
If given, the maximum time to watch a signal, in seconds. If the loop is still
|
|
331
|
+
being watched after this length, raise asyncio.TimeoutError. This should be used
|
|
332
|
+
instead of on an 'asyncio.wait_for' timeout
|
|
324
333
|
|
|
325
334
|
Notes
|
|
326
335
|
-----
|
|
327
|
-
|
|
328
|
-
|
|
336
|
+
Due to a rare condition with busy signals, it is not recommended to use this
|
|
337
|
+
function with asyncio.timeout, including in an 'asyncio.wait_for' loop. Instead,
|
|
338
|
+
this timeout should be given to the done_timeout parameter.
|
|
329
339
|
|
|
330
|
-
"""
|
|
331
|
-
actual_value = await signal.get_value()
|
|
332
|
-
assert actual_value == value, _generate_assert_error_msg(
|
|
333
|
-
name=signal.name,
|
|
334
|
-
expected_result=value,
|
|
335
|
-
actual_result=actual_value,
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
async def assert_reading(
|
|
340
|
-
readable: AsyncReadable, expected_reading: Mapping[str, Reading]
|
|
341
|
-
) -> None:
|
|
342
|
-
"""Assert readings from readable.
|
|
343
|
-
|
|
344
|
-
Parameters
|
|
345
|
-
----------
|
|
346
|
-
readable:
|
|
347
|
-
Callable with readable.read function that generate readings.
|
|
348
|
-
|
|
349
|
-
reading:
|
|
350
|
-
The expected readings from the readable.
|
|
351
|
-
|
|
352
|
-
Notes
|
|
353
|
-
-----
|
|
354
340
|
Example usage::
|
|
355
|
-
await assert_reading(readable, reading)
|
|
356
|
-
|
|
357
|
-
"""
|
|
358
|
-
actual_reading = await readable.read()
|
|
359
|
-
assert expected_reading == actual_reading, _generate_assert_error_msg(
|
|
360
|
-
name=readable.name,
|
|
361
|
-
expected_result=expected_reading,
|
|
362
|
-
actual_result=actual_reading,
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
async def assert_configuration(
|
|
367
|
-
configurable: AsyncConfigurable,
|
|
368
|
-
configuration: Mapping[str, Reading],
|
|
369
|
-
) -> None:
|
|
370
|
-
"""Assert readings from Configurable.
|
|
371
|
-
|
|
372
|
-
Parameters
|
|
373
|
-
----------
|
|
374
|
-
configurable:
|
|
375
|
-
Configurable with Configurable.read function that generate readings.
|
|
376
|
-
|
|
377
|
-
configuration:
|
|
378
|
-
The expected readings from configurable.
|
|
379
|
-
|
|
380
|
-
Notes
|
|
381
|
-
-----
|
|
382
|
-
Example usage::
|
|
383
|
-
await assert_configuration(configurable configuration)
|
|
384
341
|
|
|
342
|
+
async for value in observe_value(sig):
|
|
343
|
+
do_something_with(value)
|
|
385
344
|
"""
|
|
386
|
-
actual_configurable = await configurable.read_configuration()
|
|
387
|
-
assert configuration == actual_configurable, _generate_assert_error_msg(
|
|
388
|
-
name=configurable.name,
|
|
389
|
-
expected_result=configuration,
|
|
390
|
-
actual_result=actual_configurable,
|
|
391
|
-
)
|
|
392
|
-
|
|
393
345
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
346
|
+
async for _, value in observe_signals_value(
|
|
347
|
+
signal,
|
|
348
|
+
timeout=timeout,
|
|
349
|
+
done_status=done_status,
|
|
350
|
+
done_timeout=done_timeout,
|
|
351
|
+
):
|
|
352
|
+
yield value
|
|
401
353
|
|
|
402
|
-
numbers:
|
|
403
|
-
expected emission in kwarg from
|
|
404
354
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
"""
|
|
411
|
-
assert list(docs) == list(numbers), _generate_assert_error_msg(
|
|
412
|
-
name="documents",
|
|
413
|
-
expected_result=list(numbers),
|
|
414
|
-
actual_result=list(docs),
|
|
415
|
-
)
|
|
416
|
-
actual_numbers = {name: len(d) for name, d in docs.items()}
|
|
417
|
-
assert actual_numbers == numbers, _generate_assert_error_msg(
|
|
418
|
-
name="emitted",
|
|
419
|
-
expected_result=numbers,
|
|
420
|
-
actual_result=actual_numbers,
|
|
421
|
-
)
|
|
355
|
+
def _get_iteration_timeout(
|
|
356
|
+
timeout: float | None, overall_deadline: float | None
|
|
357
|
+
) -> float | None:
|
|
358
|
+
overall_deadline = overall_deadline - time.monotonic() if overall_deadline else None
|
|
359
|
+
return min([x for x in [overall_deadline, timeout] if x is not None], default=None)
|
|
422
360
|
|
|
423
361
|
|
|
424
|
-
async def
|
|
425
|
-
|
|
362
|
+
async def observe_signals_value(
|
|
363
|
+
*signals: SignalR[SignalDatatypeT],
|
|
426
364
|
timeout: float | None = None,
|
|
427
365
|
done_status: Status | None = None,
|
|
428
|
-
|
|
366
|
+
done_timeout: float | None = None,
|
|
367
|
+
) -> AsyncGenerator[tuple[SignalR[SignalDatatypeT], SignalDatatypeT], None]:
|
|
429
368
|
"""Subscribe to the value of a signal so it can be iterated from.
|
|
430
369
|
|
|
431
370
|
Parameters
|
|
432
371
|
----------
|
|
433
|
-
|
|
434
|
-
Call subscribe_value on
|
|
372
|
+
signals:
|
|
373
|
+
Call subscribe_value on all the signals at the start, and clear_sub on it at the
|
|
435
374
|
end
|
|
436
375
|
timeout:
|
|
437
376
|
If given, how long to wait for each updated value in seconds. If an update
|
|
@@ -439,36 +378,57 @@ async def observe_value(
|
|
|
439
378
|
done_status:
|
|
440
379
|
If this status is complete, stop observing and make the iterator return.
|
|
441
380
|
If it raises an exception then this exception will be raised by the iterator.
|
|
381
|
+
done_timeout:
|
|
382
|
+
If given, the maximum time to watch a signal, in seconds. If the loop is still
|
|
383
|
+
being watched after this length, raise asyncio.TimeoutError. This should be used
|
|
384
|
+
instead of on an 'asyncio.wait_for' timeout
|
|
442
385
|
|
|
443
386
|
Notes
|
|
444
387
|
-----
|
|
445
388
|
Example usage::
|
|
446
389
|
|
|
447
|
-
async for value in
|
|
448
|
-
|
|
390
|
+
async for signal,value in observe_signals_values(sig1,sig2,..):
|
|
391
|
+
if signal is sig1:
|
|
392
|
+
do_something_with(value)
|
|
393
|
+
elif signal is sig2:
|
|
394
|
+
do_something_else_with(value)
|
|
449
395
|
"""
|
|
396
|
+
q: asyncio.Queue[tuple[SignalR[SignalDatatypeT], SignalDatatypeT] | Status] = (
|
|
397
|
+
asyncio.Queue()
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
cbs: dict[SignalR, Callback] = {}
|
|
401
|
+
for signal in signals:
|
|
450
402
|
|
|
451
|
-
|
|
403
|
+
def queue_value(value: SignalDatatypeT, signal=signal):
|
|
404
|
+
q.put_nowait((signal, value))
|
|
405
|
+
|
|
406
|
+
cbs[signal] = queue_value
|
|
407
|
+
signal.subscribe_value(queue_value)
|
|
452
408
|
|
|
453
409
|
if done_status is not None:
|
|
454
410
|
done_status.add_callback(q.put_nowait)
|
|
455
|
-
|
|
456
|
-
signal.subscribe_value(q.put_nowait)
|
|
411
|
+
overall_deadline = time.monotonic() + done_timeout if done_timeout else None
|
|
457
412
|
try:
|
|
458
413
|
while True:
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
414
|
+
if overall_deadline and time.monotonic() >= overall_deadline:
|
|
415
|
+
raise asyncio.TimeoutError(
|
|
416
|
+
f"observe_value was still observing signals "
|
|
417
|
+
f"{[signal.source for signal in signals]} after "
|
|
418
|
+
f"timeout {done_timeout}s"
|
|
419
|
+
)
|
|
420
|
+
iteration_timeout = _get_iteration_timeout(timeout, overall_deadline)
|
|
421
|
+
item = await asyncio.wait_for(q.get(), iteration_timeout)
|
|
463
422
|
if done_status and item is done_status:
|
|
464
423
|
if exc := done_status.exception():
|
|
465
424
|
raise exc
|
|
466
425
|
else:
|
|
467
426
|
break
|
|
468
427
|
else:
|
|
469
|
-
yield cast(SignalDatatypeT, item)
|
|
428
|
+
yield cast(tuple[SignalR[SignalDatatypeT], SignalDatatypeT], item)
|
|
470
429
|
finally:
|
|
471
|
-
signal.
|
|
430
|
+
for signal, cb in cbs.items():
|
|
431
|
+
signal.clear_sub(cb)
|
|
472
432
|
|
|
473
433
|
|
|
474
434
|
class _ValueChecker(Generic[SignalDatatypeT]):
|
|
@@ -533,15 +493,16 @@ async def wait_for_value(
|
|
|
533
493
|
async def set_and_wait_for_other_value(
|
|
534
494
|
set_signal: SignalW[SignalDatatypeT],
|
|
535
495
|
set_value: SignalDatatypeT,
|
|
536
|
-
|
|
537
|
-
|
|
496
|
+
match_signal: SignalR[SignalDatatypeV],
|
|
497
|
+
match_value: SignalDatatypeV | Callable[[SignalDatatypeV], bool],
|
|
538
498
|
timeout: float = DEFAULT_TIMEOUT,
|
|
539
499
|
set_timeout: float | None = None,
|
|
500
|
+
wait_for_set_completion: bool = True,
|
|
540
501
|
) -> AsyncStatus:
|
|
541
502
|
"""Set a signal and monitor another signal until it has the specified value.
|
|
542
503
|
|
|
543
504
|
This function sets a set_signal to a specified set_value and waits for
|
|
544
|
-
a
|
|
505
|
+
a match_signal to have the match_value.
|
|
545
506
|
|
|
546
507
|
Parameters
|
|
547
508
|
----------
|
|
@@ -549,14 +510,16 @@ async def set_and_wait_for_other_value(
|
|
|
549
510
|
The signal to set
|
|
550
511
|
set_value:
|
|
551
512
|
The value to set it to
|
|
552
|
-
|
|
513
|
+
match_signal:
|
|
553
514
|
The signal to monitor
|
|
554
|
-
|
|
515
|
+
match_value:
|
|
555
516
|
The value to wait for
|
|
556
517
|
timeout:
|
|
557
518
|
How long to wait for the signal to have the value
|
|
558
519
|
set_timeout:
|
|
559
520
|
How long to wait for the set to complete
|
|
521
|
+
wait_for_set_completion:
|
|
522
|
+
This will wait for set completion #More info in how-to docs
|
|
560
523
|
|
|
561
524
|
Notes
|
|
562
525
|
-----
|
|
@@ -565,7 +528,7 @@ async def set_and_wait_for_other_value(
|
|
|
565
528
|
set_and_wait_for_value(device.acquire, 1, device.acquire_rbv, 1)
|
|
566
529
|
"""
|
|
567
530
|
# Start monitoring before the set to avoid a race condition
|
|
568
|
-
values_gen = observe_value(
|
|
531
|
+
values_gen = observe_value(match_signal)
|
|
569
532
|
|
|
570
533
|
# Get the initial value from the monitor to make sure we've created it
|
|
571
534
|
current_value = await anext(values_gen)
|
|
@@ -573,28 +536,33 @@ async def set_and_wait_for_other_value(
|
|
|
573
536
|
status = set_signal.set(set_value, timeout=set_timeout)
|
|
574
537
|
|
|
575
538
|
# If the value was the same as before no need to wait for it to change
|
|
576
|
-
if current_value !=
|
|
539
|
+
if current_value != match_value:
|
|
577
540
|
|
|
578
541
|
async def _wait_for_value():
|
|
579
542
|
async for value in values_gen:
|
|
580
|
-
if value ==
|
|
543
|
+
if value == match_value:
|
|
581
544
|
break
|
|
582
545
|
|
|
583
546
|
try:
|
|
584
547
|
await asyncio.wait_for(_wait_for_value(), timeout)
|
|
548
|
+
if wait_for_set_completion:
|
|
549
|
+
await status
|
|
550
|
+
return status
|
|
585
551
|
except asyncio.TimeoutError as e:
|
|
586
552
|
raise TimeoutError(
|
|
587
|
-
f"{
|
|
553
|
+
f"{match_signal.name} didn't match {match_value} in {timeout}s"
|
|
588
554
|
) from e
|
|
589
555
|
|
|
590
|
-
return
|
|
556
|
+
return completed_status()
|
|
591
557
|
|
|
592
558
|
|
|
593
559
|
async def set_and_wait_for_value(
|
|
594
560
|
signal: SignalRW[SignalDatatypeT],
|
|
595
561
|
value: SignalDatatypeT,
|
|
562
|
+
match_value: SignalDatatypeT | Callable[[SignalDatatypeT], bool] | None = None,
|
|
596
563
|
timeout: float = DEFAULT_TIMEOUT,
|
|
597
564
|
status_timeout: float | None = None,
|
|
565
|
+
wait_for_set_completion: bool = True,
|
|
598
566
|
) -> AsyncStatus:
|
|
599
567
|
"""Set a signal and monitor it until it has that value.
|
|
600
568
|
|
|
@@ -609,10 +577,15 @@ async def set_and_wait_for_value(
|
|
|
609
577
|
The signal to set
|
|
610
578
|
value:
|
|
611
579
|
The value to set it to
|
|
580
|
+
match_value:
|
|
581
|
+
The expected value of the signal after the operation.
|
|
582
|
+
Used to verify that the set operation was successful.
|
|
612
583
|
timeout:
|
|
613
584
|
How long to wait for the signal to have the value
|
|
614
585
|
status_timeout:
|
|
615
586
|
How long the returned Status will wait for the set to complete
|
|
587
|
+
wait_for_set_completion:
|
|
588
|
+
This will wait for set completion #More info in how-to docs
|
|
616
589
|
|
|
617
590
|
Notes
|
|
618
591
|
-----
|
|
@@ -620,6 +593,46 @@ async def set_and_wait_for_value(
|
|
|
620
593
|
|
|
621
594
|
set_and_wait_for_value(device.acquire, 1)
|
|
622
595
|
"""
|
|
596
|
+
if match_value is None:
|
|
597
|
+
match_value = value
|
|
623
598
|
return await set_and_wait_for_other_value(
|
|
624
|
-
signal,
|
|
599
|
+
signal,
|
|
600
|
+
value,
|
|
601
|
+
signal,
|
|
602
|
+
match_value,
|
|
603
|
+
timeout,
|
|
604
|
+
status_timeout,
|
|
605
|
+
wait_for_set_completion,
|
|
625
606
|
)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def walk_rw_signals(device: Device, path_prefix: str = "") -> dict[str, SignalRW[Any]]:
|
|
610
|
+
"""Retrieve all SignalRWs from a device.
|
|
611
|
+
|
|
612
|
+
Stores retrieved signals with their dotted attribute paths in a dictionary. Used as
|
|
613
|
+
part of saving and loading a device.
|
|
614
|
+
|
|
615
|
+
Parameters
|
|
616
|
+
----------
|
|
617
|
+
device : Device
|
|
618
|
+
Ophyd device to retrieve read-write signals from.
|
|
619
|
+
|
|
620
|
+
path_prefix : str
|
|
621
|
+
For internal use, leave blank when calling the method.
|
|
622
|
+
|
|
623
|
+
Returns
|
|
624
|
+
-------
|
|
625
|
+
SignalRWs : dict
|
|
626
|
+
A dictionary matching the string attribute path of a SignalRW with the
|
|
627
|
+
signal itself.
|
|
628
|
+
|
|
629
|
+
"""
|
|
630
|
+
signals: dict[str, SignalRW[Any]] = {}
|
|
631
|
+
|
|
632
|
+
for attr_name, attr in device.children():
|
|
633
|
+
dot_path = f"{path_prefix}{attr_name}"
|
|
634
|
+
if type(attr) is SignalRW:
|
|
635
|
+
signals[dot_path] = attr
|
|
636
|
+
attr_signals = walk_rw_signals(attr, path_prefix=dot_path + ".")
|
|
637
|
+
signals.update(attr_signals)
|
|
638
|
+
return signals
|
|
@@ -10,7 +10,10 @@ from ._table import Table
|
|
|
10
10
|
from ._utils import Callback, StrictEnum, T
|
|
11
11
|
|
|
12
12
|
DTypeScalar_co = TypeVar("DTypeScalar_co", covariant=True, bound=np.generic)
|
|
13
|
-
|
|
13
|
+
# To be a 1D array shape should really be tuple[int], but np.array()
|
|
14
|
+
# currently produces tuple[int, ...] even when it has 1D input args
|
|
15
|
+
# https://github.com/numpy/numpy/issues/28077#issuecomment-2566485178
|
|
16
|
+
Array1D = np.ndarray[tuple[int, ...], np.dtype[DTypeScalar_co]]
|
|
14
17
|
Primitive = bool | int | float | str
|
|
15
18
|
# NOTE: if you change this union then update the docs to match
|
|
16
19
|
SignalDatatype = (
|
|
@@ -175,7 +175,8 @@ class SoftSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
|
175
175
|
return self.reading["value"]
|
|
176
176
|
|
|
177
177
|
def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
|
|
178
|
+
if callback and self.callback:
|
|
179
|
+
raise RuntimeError("Cannot set a callback when one is already set")
|
|
178
180
|
if callback:
|
|
179
|
-
assert not self.callback, "Cannot set a callback when one is already set"
|
|
180
181
|
callback(self.reading)
|
|
181
182
|
self.callback = callback
|
ophyd_async/core/_table.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import Sequence
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
4
|
from typing import Annotated, Any, TypeVar, get_origin
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
@@ -19,6 +19,13 @@ def _concat(value1, value2):
|
|
|
19
19
|
return value1 + value2
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def _make_default_factory(dtype: np.dtype) -> Callable[[], np.ndarray]:
|
|
23
|
+
def numpy_array_default_factory() -> np.ndarray:
|
|
24
|
+
return np.array([], dtype)
|
|
25
|
+
|
|
26
|
+
return numpy_array_default_factory
|
|
27
|
+
|
|
28
|
+
|
|
22
29
|
class Table(BaseModel):
|
|
23
30
|
"""An abstraction of a Table of str to numpy array."""
|
|
24
31
|
|
|
@@ -32,6 +39,11 @@ class Table(BaseModel):
|
|
|
32
39
|
# so it is strictly checked against the BaseModel we are supplied.
|
|
33
40
|
model_config = ConfigDict(extra="allow")
|
|
34
41
|
|
|
42
|
+
# Add an init method to match the above model config, otherwise the type
|
|
43
|
+
# checker will not think we can pass arbitrary kwargs into the base class init
|
|
44
|
+
def __init__(self, **kwargs):
|
|
45
|
+
super().__init__(**kwargs)
|
|
46
|
+
|
|
35
47
|
@classmethod
|
|
36
48
|
def __init_subclass__(cls):
|
|
37
49
|
# But forbit extra in subclasses so it gets validated
|
|
@@ -45,9 +57,7 @@ class Table(BaseModel):
|
|
|
45
57
|
NpArrayPydanticAnnotation.factory(
|
|
46
58
|
data_type=dtype.type, dimensions=1, strict_data_typing=False
|
|
47
59
|
),
|
|
48
|
-
Field(
|
|
49
|
-
default_factory=lambda dtype=dtype: np.array([], dtype=dtype)
|
|
50
|
-
),
|
|
60
|
+
Field(default_factory=_make_default_factory(dtype)),
|
|
51
61
|
]
|
|
52
62
|
elif get_origin(anno) is Sequence:
|
|
53
63
|
new_anno = Annotated[anno, Field(default_factory=list)]
|
|
@@ -73,9 +83,6 @@ class Table(BaseModel):
|
|
|
73
83
|
}
|
|
74
84
|
)
|
|
75
85
|
|
|
76
|
-
def __eq__(self, value: object) -> bool:
|
|
77
|
-
return super().__eq__(value)
|
|
78
|
-
|
|
79
86
|
def numpy_dtype(self) -> np.dtype:
|
|
80
87
|
dtype = []
|
|
81
88
|
for k, v in self:
|
|
@@ -94,8 +101,10 @@ class Table(BaseModel):
|
|
|
94
101
|
v = v[selection]
|
|
95
102
|
if array is None:
|
|
96
103
|
array = np.empty(v.shape, dtype=self.numpy_dtype())
|
|
97
|
-
array[k] = v
|
|
98
|
-
|
|
104
|
+
array[k] = v # type: ignore
|
|
105
|
+
if array is None:
|
|
106
|
+
msg = "No arrays found in table"
|
|
107
|
+
raise ValueError(msg)
|
|
99
108
|
return array
|
|
100
109
|
|
|
101
110
|
@model_validator(mode="before")
|
|
@@ -118,10 +127,12 @@ class Table(BaseModel):
|
|
|
118
127
|
# Convert to correct dtype, but only if we don't lose precision
|
|
119
128
|
# as a result
|
|
120
129
|
cast_value = np.array(data_value).astype(expected_dtype)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
if not np.array_equal(data_value, cast_value):
|
|
131
|
+
msg = (
|
|
132
|
+
f"{field_name}: Cannot cast {data_value} to {expected_dtype} "
|
|
133
|
+
"without losing precision"
|
|
134
|
+
)
|
|
135
|
+
raise ValueError(msg)
|
|
125
136
|
data_dict[field_name] = cast_value
|
|
126
137
|
return data_dict
|
|
127
138
|
|
|
@@ -130,7 +141,9 @@ class Table(BaseModel):
|
|
|
130
141
|
lengths: dict[int, set[str]] = {}
|
|
131
142
|
for field_name, field_value in self:
|
|
132
143
|
lengths.setdefault(len(field_value), set()).add(field_name)
|
|
133
|
-
|
|
144
|
+
if len(lengths) > 1:
|
|
145
|
+
msg = f"Columns should be same length, got {lengths=}"
|
|
146
|
+
raise ValueError(msg)
|
|
134
147
|
return self
|
|
135
148
|
|
|
136
149
|
def __len__(self) -> int:
|