ophyd-async 0.8.0a6__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 +15 -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 +91 -151
- ophyd_async/core/_signal_backend.py +4 -1
- ophyd_async/core/_soft_signal_backend.py +2 -1
- ophyd_async/core/_table.py +18 -10
- 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 -86
- 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 +4 -4
- 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 +14 -14
- ophyd_async/epics/testing/_example_ioc.py +53 -67
- ophyd_async/epics/testing/_utils.py +17 -45
- ophyd_async/epics/testing/test_records.db +22 -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/core/_signal.py +3 -1
- ophyd_async/tango/core/_tango_transport.py +13 -15
- ophyd_async/tango/{demo → sim}/_mover.py +5 -2
- 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.0a6.dist-info → ophyd_async-0.9.0.dist-info}/METADATA +4 -2
- ophyd_async-0.9.0.dist-info/RECORD +129 -0
- {ophyd_async-0.8.0a6.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-0.8.0a6.dist-info/RECORD +0 -116
- ophyd_async-0.8.0a6.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}/_counter.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.0a6.dist-info → ophyd_async-0.9.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
3
|
from ophyd_async.core import (
|
|
4
|
-
AsyncStatus,
|
|
5
|
-
DetectorController,
|
|
6
4
|
DetectorTrigger,
|
|
7
5
|
TriggerInfo,
|
|
8
6
|
)
|
|
@@ -11,56 +9,44 @@ from ophyd_async.epics import adcore
|
|
|
11
9
|
from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource
|
|
12
10
|
|
|
13
11
|
TRIGGER_MODE = {
|
|
14
|
-
DetectorTrigger.
|
|
15
|
-
DetectorTrigger.
|
|
16
|
-
DetectorTrigger.
|
|
17
|
-
DetectorTrigger.
|
|
12
|
+
DetectorTrigger.INTERNAL: VimbaOnOff.OFF,
|
|
13
|
+
DetectorTrigger.CONSTANT_GATE: VimbaOnOff.ON,
|
|
14
|
+
DetectorTrigger.VARIABLE_GATE: VimbaOnOff.ON,
|
|
15
|
+
DetectorTrigger.EDGE_TRIGGER: VimbaOnOff.ON,
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
EXPOSE_OUT_MODE = {
|
|
21
|
-
DetectorTrigger.
|
|
22
|
-
DetectorTrigger.
|
|
23
|
-
DetectorTrigger.
|
|
24
|
-
DetectorTrigger.
|
|
19
|
+
DetectorTrigger.INTERNAL: VimbaExposeOutMode.TIMED,
|
|
20
|
+
DetectorTrigger.CONSTANT_GATE: VimbaExposeOutMode.TRIGGER_WIDTH,
|
|
21
|
+
DetectorTrigger.VARIABLE_GATE: VimbaExposeOutMode.TRIGGER_WIDTH,
|
|
22
|
+
DetectorTrigger.EDGE_TRIGGER: VimbaExposeOutMode.TIMED,
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
|
|
28
|
-
class VimbaController(
|
|
26
|
+
class VimbaController(adcore.ADBaseController[VimbaDriverIO]):
|
|
29
27
|
def __init__(
|
|
30
28
|
self,
|
|
31
29
|
driver: VimbaDriverIO,
|
|
30
|
+
good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
|
|
32
31
|
) -> None:
|
|
33
|
-
|
|
34
|
-
self._arm_status: AsyncStatus | None = None
|
|
32
|
+
super().__init__(driver, good_states=good_states)
|
|
35
33
|
|
|
36
34
|
def get_deadtime(self, exposure: float | None) -> float:
|
|
37
35
|
return 0.001
|
|
38
36
|
|
|
39
37
|
async def prepare(self, trigger_info: TriggerInfo):
|
|
40
38
|
await asyncio.gather(
|
|
41
|
-
self.
|
|
42
|
-
self.
|
|
43
|
-
self.
|
|
44
|
-
self.
|
|
39
|
+
self.driver.trigger_mode.set(TRIGGER_MODE[trigger_info.trigger]),
|
|
40
|
+
self.driver.exposure_mode.set(EXPOSE_OUT_MODE[trigger_info.trigger]),
|
|
41
|
+
self.driver.num_images.set(trigger_info.total_number_of_triggers),
|
|
42
|
+
self.driver.image_mode.set(adcore.ImageMode.MULTIPLE),
|
|
45
43
|
)
|
|
46
44
|
if trigger_info.livetime is not None and trigger_info.trigger not in [
|
|
47
|
-
DetectorTrigger.
|
|
48
|
-
DetectorTrigger.
|
|
45
|
+
DetectorTrigger.VARIABLE_GATE,
|
|
46
|
+
DetectorTrigger.CONSTANT_GATE,
|
|
49
47
|
]:
|
|
50
|
-
await self.
|
|
51
|
-
if trigger_info.trigger != DetectorTrigger.
|
|
52
|
-
self.
|
|
48
|
+
await self.driver.acquire_time.set(trigger_info.livetime)
|
|
49
|
+
if trigger_info.trigger != DetectorTrigger.INTERNAL:
|
|
50
|
+
self.driver.trigger_source.set(VimbaTriggerSource.LINE1)
|
|
53
51
|
else:
|
|
54
|
-
self.
|
|
55
|
-
|
|
56
|
-
async def arm(self):
|
|
57
|
-
self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
|
|
58
|
-
self._drv
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
async def wait_for_idle(self):
|
|
62
|
-
if self._arm_status:
|
|
63
|
-
await self._arm_status
|
|
64
|
-
|
|
65
|
-
async def disarm(self):
|
|
66
|
-
await adcore.stop_busy_record(self._drv.acquire, False, timeout=1)
|
|
52
|
+
self.driver.trigger_source.set(VimbaTriggerSource.FREERUN)
|
|
@@ -4,44 +4,44 @@ from ophyd_async.epics.core import epics_signal_rw_rbv
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class VimbaPixelFormat(StrictEnum):
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
INTERNAL = "Mono8"
|
|
8
|
+
EXT_ENABLE = "Mono12"
|
|
9
|
+
EXT_TRIGGER = "Ext. Trigger"
|
|
10
|
+
MULT_TRIGGER = "Mult. Trigger"
|
|
11
|
+
ALIGNMENT = "Alignment"
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class VimbaConvertFormat(StrictEnum):
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
NONE = "None"
|
|
16
|
+
MONO8 = "Mono8"
|
|
17
|
+
MONO16 = "Mono16"
|
|
18
|
+
RGB8 = "RGB8"
|
|
19
|
+
RGB16 = "RGB16"
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class VimbaTriggerSource(StrictEnum):
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
FREERUN = "Freerun"
|
|
24
|
+
LINE1 = "Line1"
|
|
25
|
+
LINE2 = "Line2"
|
|
26
|
+
FIXED_RATE = "FixedRate"
|
|
27
|
+
SOFTWARE = "Software"
|
|
28
|
+
ACTION0 = "Action0"
|
|
29
|
+
ACTION1 = "Action1"
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class VimbaOverlap(StrictEnum):
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
OFF = "Off"
|
|
34
|
+
PREV_FRAME = "PreviousFrame"
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
class VimbaOnOff(StrictEnum):
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
ON = "On"
|
|
39
|
+
OFF = "Off"
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
class VimbaExposeOutMode(StrictEnum):
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
TIMED = "Timed" # Use ExposureTime PV
|
|
44
|
+
TRIGGER_WIDTH = "TriggerWidth" # Expose for length of high signal
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
class VimbaDriverIO(adcore.ADBaseIO):
|
ophyd_async/epics/core/_aioca.py
CHANGED
|
@@ -35,12 +35,14 @@ from ophyd_async.core import (
|
|
|
35
35
|
|
|
36
36
|
from ._util import EpicsSignalBackend, format_datatype, get_supported_values
|
|
37
37
|
|
|
38
|
+
logger = logging.getLogger("ophyd_async")
|
|
39
|
+
|
|
38
40
|
|
|
39
41
|
def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
|
|
40
42
|
def get_limits(limit: str) -> LimitsRange | None:
|
|
41
43
|
low = getattr(value, f"lower_{limit}_limit", nan)
|
|
42
44
|
high = getattr(value, f"upper_{limit}_limit", nan)
|
|
43
|
-
if not (isnan(low) and isnan(high)):
|
|
45
|
+
if not (isnan(low) and isnan(high)) and not high == low == 0:
|
|
44
46
|
return LimitsRange(
|
|
45
47
|
low=None if isnan(low) else low,
|
|
46
48
|
high=None if isnan(high) else high,
|
|
@@ -59,14 +61,20 @@ def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
|
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
def _metadata_from_augmented_value(
|
|
62
|
-
|
|
64
|
+
datatype: type[SignalDatatypeT] | None,
|
|
65
|
+
value: AugmentedValue,
|
|
66
|
+
metadata: SignalMetadata,
|
|
63
67
|
) -> SignalMetadata:
|
|
64
68
|
metadata = metadata.copy()
|
|
65
|
-
if hasattr(value, "units"):
|
|
69
|
+
if hasattr(value, "units") and datatype not in (str, bool):
|
|
66
70
|
metadata["units"] = value.units
|
|
67
|
-
if
|
|
71
|
+
if (
|
|
72
|
+
hasattr(value, "precision")
|
|
73
|
+
and not isnan(value.precision)
|
|
74
|
+
and datatype is not int
|
|
75
|
+
):
|
|
68
76
|
metadata["precision"] = value.precision
|
|
69
|
-
if limits := _limits_from_augmented_value(value):
|
|
77
|
+
if (limits := _limits_from_augmented_value(value)) and datatype is not bool:
|
|
70
78
|
metadata["limits"] = limits
|
|
71
79
|
return metadata
|
|
72
80
|
|
|
@@ -100,6 +108,11 @@ class DisconnectedCaConverter(CaConverter):
|
|
|
100
108
|
raise NotImplementedError("No PV has been set as connect() has not been called")
|
|
101
109
|
|
|
102
110
|
|
|
111
|
+
class CaIntConverter(CaConverter[int]):
|
|
112
|
+
def value(self, value: AugmentedValue) -> int:
|
|
113
|
+
return int(value) # type: ignore
|
|
114
|
+
|
|
115
|
+
|
|
103
116
|
class CaArrayConverter(CaConverter[np.ndarray]):
|
|
104
117
|
def value(self, value: AugmentedValue) -> np.ndarray:
|
|
105
118
|
# A less expensive conversion
|
|
@@ -202,7 +215,7 @@ def make_converter(
|
|
|
202
215
|
and get_unique({k: v.precision for k, v in values.items()}, "precision") == 0
|
|
203
216
|
):
|
|
204
217
|
# Allow int signals to represent float records when prec is 0
|
|
205
|
-
return
|
|
218
|
+
return CaIntConverter(int, pv_dbr)
|
|
206
219
|
elif datatype in (None, inferred_datatype):
|
|
207
220
|
# If datatype matches what we are given then allow it and use inferred converter
|
|
208
221
|
return converter_cls(inferred_datatype, pv_dbr)
|
|
@@ -247,7 +260,7 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
247
260
|
pv, format=FORMAT_CTRL, timeout=timeout
|
|
248
261
|
)
|
|
249
262
|
except CANothing as exc:
|
|
250
|
-
|
|
263
|
+
logger.debug(f"signal ca://{pv} timed out")
|
|
251
264
|
raise NotConnected(f"ca://{pv}") from exc
|
|
252
265
|
|
|
253
266
|
async def connect(self, timeout: float):
|
|
@@ -280,17 +293,33 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
280
293
|
write_value = self.initial_values[self.write_pv]
|
|
281
294
|
else:
|
|
282
295
|
write_value = self.converter.write_value(value)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
296
|
+
try:
|
|
297
|
+
await caput(
|
|
298
|
+
self.write_pv,
|
|
299
|
+
write_value,
|
|
300
|
+
datatype=self.converter.write_dbr,
|
|
301
|
+
wait=wait,
|
|
302
|
+
timeout=None,
|
|
303
|
+
)
|
|
304
|
+
except CANothing as exc:
|
|
305
|
+
# If we ran into a write error, check to see if there is a list
|
|
306
|
+
# of valid choices, and if the value we tried to write is in that list.
|
|
307
|
+
valid_choices = self.converter.metadata.get("choices")
|
|
308
|
+
if valid_choices:
|
|
309
|
+
if value not in valid_choices:
|
|
310
|
+
msg = (
|
|
311
|
+
f"{value} is not a valid choice for {self.write_pv}, "
|
|
312
|
+
f"valid choices: {self.converter.metadata.get('choices')}"
|
|
313
|
+
)
|
|
314
|
+
raise ValueError(msg) from exc
|
|
315
|
+
raise
|
|
316
|
+
raise
|
|
290
317
|
|
|
291
318
|
async def get_datakey(self, source: str) -> DataKey:
|
|
292
319
|
value = await self._caget(self.read_pv, FORMAT_CTRL)
|
|
293
|
-
metadata = _metadata_from_augmented_value(
|
|
320
|
+
metadata = _metadata_from_augmented_value(
|
|
321
|
+
self.datatype, value, self.converter.metadata
|
|
322
|
+
)
|
|
294
323
|
return make_datakey(
|
|
295
324
|
self.converter.datatype, self.converter.value(value), source, metadata
|
|
296
325
|
)
|
|
@@ -308,16 +337,18 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
308
337
|
return self.converter.value(value)
|
|
309
338
|
|
|
310
339
|
def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
|
|
340
|
+
if callback and self.subscription:
|
|
341
|
+
msg = "Cannot set a callback when one is already set"
|
|
342
|
+
raise RuntimeError(msg)
|
|
343
|
+
|
|
344
|
+
if self.subscription:
|
|
345
|
+
self.subscription.close()
|
|
346
|
+
self.subscription = None
|
|
347
|
+
|
|
311
348
|
if callback:
|
|
312
|
-
assert (
|
|
313
|
-
not self.subscription
|
|
314
|
-
), "Cannot set a callback when one is already set"
|
|
315
349
|
self.subscription = camonitor(
|
|
316
350
|
self.read_pv,
|
|
317
351
|
lambda v: callback(self._make_reading(v)),
|
|
318
352
|
datatype=self.converter.read_dbr,
|
|
319
353
|
format=FORMAT_TIME,
|
|
320
354
|
)
|
|
321
|
-
elif self.subscription:
|
|
322
|
-
self.subscription.close()
|
|
323
|
-
self.subscription = None
|
ophyd_async/epics/core/_p4p.py
CHANGED
|
@@ -31,6 +31,8 @@ from ophyd_async.core import (
|
|
|
31
31
|
|
|
32
32
|
from ._util import EpicsSignalBackend, format_datatype, get_supported_values
|
|
33
33
|
|
|
34
|
+
logger = logging.getLogger("ophyd_async")
|
|
35
|
+
|
|
34
36
|
|
|
35
37
|
def _limits_from_value(value: Any) -> Limits:
|
|
36
38
|
def get_limits(
|
|
@@ -39,7 +41,7 @@ def _limits_from_value(value: Any) -> Limits:
|
|
|
39
41
|
substructure = getattr(value, substucture_name, None)
|
|
40
42
|
low = getattr(substructure, low_name, nan)
|
|
41
43
|
high = getattr(substructure, high_name, nan)
|
|
42
|
-
if not (isnan(low) and isnan(high)):
|
|
44
|
+
if not (isnan(low) and isnan(high)) and not low == high == 0:
|
|
43
45
|
return LimitsRange(
|
|
44
46
|
low=None if isnan(low) else low,
|
|
45
47
|
high=None if isnan(high) else high,
|
|
@@ -60,12 +62,22 @@ def _limits_from_value(value: Any) -> Limits:
|
|
|
60
62
|
def _metadata_from_value(datatype: type[SignalDatatype], value: Any) -> SignalMetadata:
|
|
61
63
|
metadata = SignalMetadata()
|
|
62
64
|
value_data: Any = getattr(value, "value", None)
|
|
65
|
+
specifier = _get_specifier(value)
|
|
63
66
|
display_data: Any = getattr(value, "display", None)
|
|
64
|
-
if
|
|
67
|
+
if (
|
|
68
|
+
hasattr(display_data, "units")
|
|
69
|
+
and specifier[-1] in _number_specifiers
|
|
70
|
+
and datatype is not str
|
|
71
|
+
):
|
|
65
72
|
metadata["units"] = display_data.units
|
|
66
|
-
if
|
|
73
|
+
if (
|
|
74
|
+
hasattr(display_data, "precision")
|
|
75
|
+
and not isnan(display_data.precision)
|
|
76
|
+
and specifier[-1] in _float_specifiers
|
|
77
|
+
and datatype is not int
|
|
78
|
+
):
|
|
67
79
|
metadata["precision"] = display_data.precision
|
|
68
|
-
if limits := _limits_from_value(value):
|
|
80
|
+
if (limits := _limits_from_value(value)) and specifier[-1] in _number_specifiers:
|
|
69
81
|
metadata["limits"] = limits
|
|
70
82
|
# Get choices from display or value
|
|
71
83
|
if datatype is str or issubclass(datatype, StrictEnum):
|
|
@@ -84,9 +96,7 @@ class PvaConverter(Generic[SignalDatatypeT]):
|
|
|
84
96
|
self.datatype = datatype
|
|
85
97
|
|
|
86
98
|
def value(self, value: Any) -> SignalDatatypeT:
|
|
87
|
-
#
|
|
88
|
-
# invokes __pos__ operator to return an instance of
|
|
89
|
-
# the builtin base class
|
|
99
|
+
# Normally the value will be of the correct python type
|
|
90
100
|
return value["value"]
|
|
91
101
|
|
|
92
102
|
def write_value(self, value: Any) -> Any:
|
|
@@ -94,6 +104,31 @@ class PvaConverter(Generic[SignalDatatypeT]):
|
|
|
94
104
|
return value
|
|
95
105
|
|
|
96
106
|
|
|
107
|
+
class PvaIntConverter(PvaConverter[int]):
|
|
108
|
+
def __init__(self):
|
|
109
|
+
super().__init__(int)
|
|
110
|
+
|
|
111
|
+
def value(self, value: Any) -> int:
|
|
112
|
+
# Convert to an int
|
|
113
|
+
return int(value["value"])
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class PvaLongStringConverter(PvaConverter[str]):
|
|
117
|
+
def __init__(self):
|
|
118
|
+
super().__init__(str)
|
|
119
|
+
|
|
120
|
+
def value(self, value: Any) -> Any:
|
|
121
|
+
# Value here is a null terminated array of ascii codes.
|
|
122
|
+
# We strip out the null terminator, and convert each code
|
|
123
|
+
# to the corresponding char, joining into a string
|
|
124
|
+
return value["value"].tobytes().rstrip(b"\0").decode()
|
|
125
|
+
|
|
126
|
+
def write_value(self, value: Any) -> Any:
|
|
127
|
+
# Inverse of reading - convert each character into it's ascii code,
|
|
128
|
+
# put into a list, and add null terminator.
|
|
129
|
+
return np.frombuffer(str(value).encode() + b"\0", dtype=np.int8)
|
|
130
|
+
|
|
131
|
+
|
|
97
132
|
class DisconnectedPvaConverter(PvaConverter):
|
|
98
133
|
def __getattribute__(self, __name: str) -> Any:
|
|
99
134
|
raise NotImplementedError("No PV has been set as connect() has not been called")
|
|
@@ -158,6 +193,9 @@ class PvaTableConverter(PvaConverter[Table]):
|
|
|
158
193
|
|
|
159
194
|
|
|
160
195
|
# https://mdavidsaver.github.io/p4p/values.html
|
|
196
|
+
_float_specifiers = {"f", "d"}
|
|
197
|
+
_int_specifiers = {"b", "B", "h", "H", "i", "I", "l", "L"}
|
|
198
|
+
_number_specifiers = _float_specifiers.union(_int_specifiers)
|
|
161
199
|
_datatype_converter_from_typeid: dict[
|
|
162
200
|
tuple[str, str], tuple[type[SignalDatatype], type[PvaConverter]]
|
|
163
201
|
] = {
|
|
@@ -192,7 +230,7 @@ _datatype_converter_from_typeid: dict[
|
|
|
192
230
|
}
|
|
193
231
|
|
|
194
232
|
|
|
195
|
-
def _get_specifier(value: Value):
|
|
233
|
+
def _get_specifier(value: Value) -> str:
|
|
196
234
|
typ = value.type("value").aspy()
|
|
197
235
|
if isinstance(typ, tuple):
|
|
198
236
|
return typ[0]
|
|
@@ -242,7 +280,7 @@ def make_converter(datatype: type | None, values: dict[str, Any]) -> PvaConverte
|
|
|
242
280
|
== 0
|
|
243
281
|
):
|
|
244
282
|
# Allow int signals to represent float records when prec is 0
|
|
245
|
-
return
|
|
283
|
+
return PvaIntConverter()
|
|
246
284
|
elif inferred_datatype is str and (enum_cls := get_enum_cls(datatype)):
|
|
247
285
|
# Allow strings to be used as enums until QSRV supports this
|
|
248
286
|
return PvaConverter(str)
|
|
@@ -252,6 +290,9 @@ def make_converter(datatype: type | None, values: dict[str, Any]) -> PvaConverte
|
|
|
252
290
|
elif datatype in (None, inferred_datatype):
|
|
253
291
|
# If datatype matches what we are given then allow it and use inferred converter
|
|
254
292
|
return converter_cls(inferred_datatype)
|
|
293
|
+
# Allow waveforms with FTVL=CHAR to be treated as str when requested
|
|
294
|
+
elif datatype is str and inferred_datatype == Array1D[np.int8]:
|
|
295
|
+
return PvaLongStringConverter()
|
|
255
296
|
raise TypeError(
|
|
256
297
|
f"{pv} with inferred datatype {format_datatype(inferred_datatype)}"
|
|
257
298
|
f" from {typeid=} {specifier=}"
|
|
@@ -282,7 +323,7 @@ async def pvget_with_timeout(pv: str, timeout: float) -> Any:
|
|
|
282
323
|
try:
|
|
283
324
|
return await asyncio.wait_for(context().get(pv), timeout=timeout)
|
|
284
325
|
except asyncio.TimeoutError as exc:
|
|
285
|
-
|
|
326
|
+
logger.debug(f"signal pva://{pv} timed out", exc_info=True)
|
|
286
327
|
raise NotConnected(f"pva://{pv}") from exc
|
|
287
328
|
|
|
288
329
|
|
|
@@ -364,10 +405,15 @@ class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
364
405
|
return self.converter.value(value)
|
|
365
406
|
|
|
366
407
|
def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
|
|
408
|
+
if callback and self.subscription:
|
|
409
|
+
msg = "Cannot set a callback when one is already set"
|
|
410
|
+
raise RuntimeError(msg)
|
|
411
|
+
|
|
412
|
+
if self.subscription:
|
|
413
|
+
self.subscription.close()
|
|
414
|
+
self.subscription = None
|
|
415
|
+
|
|
367
416
|
if callback:
|
|
368
|
-
assert (
|
|
369
|
-
not self.subscription
|
|
370
|
-
), "Cannot set a callback when one is already set"
|
|
371
417
|
|
|
372
418
|
async def async_callback(v):
|
|
373
419
|
callback(self._make_reading(v))
|
|
@@ -378,6 +424,3 @@ class PvaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
|
378
424
|
self.subscription = context().monitor(
|
|
379
425
|
self.read_pv, async_callback, request=request
|
|
380
426
|
)
|
|
381
|
-
elif self.subscription:
|
|
382
|
-
self.subscription.close()
|
|
383
|
-
self.subscription = None
|
|
@@ -32,10 +32,11 @@ def _get_signal_details(entry: Entry) -> tuple[type[Signal], str, str]:
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
class PviDeviceConnector(DeviceConnector):
|
|
35
|
-
def __init__(self, prefix: str = "") -> None:
|
|
35
|
+
def __init__(self, prefix: str = "", error_hint: str = "") -> None:
|
|
36
36
|
# TODO: what happens if we get a leading "pva://" here?
|
|
37
37
|
self.prefix = prefix
|
|
38
38
|
self.pvi_pv = prefix + "PVI"
|
|
39
|
+
self.error_hint = error_hint
|
|
39
40
|
|
|
40
41
|
def create_children_from_annotations(self, device: Device):
|
|
41
42
|
if not hasattr(self, "filler"):
|
|
@@ -85,7 +86,8 @@ class PviDeviceConnector(DeviceConnector):
|
|
|
85
86
|
if e:
|
|
86
87
|
self._fill_child(name, e, i)
|
|
87
88
|
# Check that all the requested children have been filled
|
|
88
|
-
|
|
89
|
+
suffix = f"\n{self.error_hint}" if self.error_hint else ""
|
|
90
|
+
self.filler.check_filled(f"{self.pvi_pv}: {entries}{suffix}")
|
|
89
91
|
# Set the name of the device to name all children
|
|
90
92
|
device.set_name(device.name)
|
|
91
93
|
return await super().connect_real(device, timeout, force_reconnect)
|
|
@@ -14,7 +14,7 @@ from ophyd_async.core import (
|
|
|
14
14
|
get_unique,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
from ._util import EpicsSignalBackend
|
|
17
|
+
from ._util import EpicsSignalBackend, get_pv_basename_and_field
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class EpicsProtocol(Enum):
|
|
@@ -124,7 +124,14 @@ def epics_signal_rw_rbv(
|
|
|
124
124
|
read_suffix:
|
|
125
125
|
Append this suffix to the write pv to create the readback pv
|
|
126
126
|
"""
|
|
127
|
-
|
|
127
|
+
|
|
128
|
+
base_pv, field = get_pv_basename_and_field(write_pv)
|
|
129
|
+
if field is not None:
|
|
130
|
+
read_pv = f"{base_pv}{read_suffix}.{field}"
|
|
131
|
+
else:
|
|
132
|
+
read_pv = f"{write_pv}{read_suffix}"
|
|
133
|
+
|
|
134
|
+
return epics_signal_rw(datatype, read_pv, write_pv, name)
|
|
128
135
|
|
|
129
136
|
|
|
130
137
|
def epics_signal_r(
|
ophyd_async/epics/core/_util.py
CHANGED
|
@@ -12,6 +12,15 @@ from ophyd_async.core import (
|
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def get_pv_basename_and_field(pv: str) -> tuple[str, str | None]:
|
|
16
|
+
"""Simple utility function for extracting base pv name without field"""
|
|
17
|
+
|
|
18
|
+
if "." in pv:
|
|
19
|
+
return (pv.split(".", -1)[0], pv.split(".", -1)[1])
|
|
20
|
+
else:
|
|
21
|
+
return (pv, None)
|
|
22
|
+
|
|
23
|
+
|
|
15
24
|
def get_supported_values(
|
|
16
25
|
pv: str,
|
|
17
26
|
datatype: type,
|
|
@@ -38,7 +47,7 @@ def get_supported_values(
|
|
|
38
47
|
|
|
39
48
|
|
|
40
49
|
def format_datatype(datatype: Any) -> str:
|
|
41
|
-
if get_origin(datatype) is np.ndarray and get_args(datatype)
|
|
50
|
+
if get_origin(datatype) is np.ndarray and get_args(datatype):
|
|
42
51
|
dtype = get_dtype(datatype)
|
|
43
52
|
return f"Array1D[np.{dtype.name}]"
|
|
44
53
|
elif get_origin(datatype) is Sequence:
|
|
@@ -11,10 +11,10 @@ from ophyd_async.core import (
|
|
|
11
11
|
from ._eiger_io import EigerDriverIO, EigerTriggerMode
|
|
12
12
|
|
|
13
13
|
EIGER_TRIGGER_MODE_MAP = {
|
|
14
|
-
DetectorTrigger.
|
|
15
|
-
DetectorTrigger.
|
|
16
|
-
DetectorTrigger.
|
|
17
|
-
DetectorTrigger.
|
|
14
|
+
DetectorTrigger.INTERNAL: EigerTriggerMode.INTERNAL,
|
|
15
|
+
DetectorTrigger.CONSTANT_GATE: EigerTriggerMode.GATE,
|
|
16
|
+
DetectorTrigger.VARIABLE_GATE: EigerTriggerMode.GATE,
|
|
17
|
+
DetectorTrigger.EDGE_TRIGGER: EigerTriggerMode.EDGE,
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
|
|
@@ -3,9 +3,9 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw_rbv, epics_si
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class EigerTriggerMode(StrictEnum):
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
INTERNAL = "ints"
|
|
7
|
+
EDGE = "exts"
|
|
8
|
+
GATE = "exte"
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class EigerDriverIO(Device):
|
ophyd_async/epics/motor.py
CHANGED
|
@@ -20,7 +20,7 @@ from ophyd_async.core import (
|
|
|
20
20
|
observe_value,
|
|
21
21
|
)
|
|
22
22
|
from ophyd_async.core import StandardReadableFormat as Format
|
|
23
|
-
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw,
|
|
23
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class MotorLimitsException(Exception):
|
|
@@ -63,6 +63,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
63
63
|
with self.add_children_as_readables(Format.CONFIG_SIGNAL):
|
|
64
64
|
self.motor_egu = epics_signal_r(str, prefix + ".EGU")
|
|
65
65
|
self.velocity = epics_signal_rw(float, prefix + ".VELO")
|
|
66
|
+
self.offset = epics_signal_rw(float, prefix + ".OFF")
|
|
66
67
|
|
|
67
68
|
with self.add_children_as_readables(Format.HINTED_SIGNAL):
|
|
68
69
|
self.user_readback = epics_signal_r(float, prefix + ".RBV")
|
|
@@ -76,7 +77,10 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
76
77
|
self.low_limit_travel = epics_signal_rw(float, prefix + ".LLM")
|
|
77
78
|
self.high_limit_travel = epics_signal_rw(float, prefix + ".HLM")
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
# Note:cannot use epics_signal_x here, as the motor record specifies that
|
|
81
|
+
# we must write 1 to stop the motor. Simply processing the record is not
|
|
82
|
+
# sufficient.
|
|
83
|
+
self.motor_stop = epics_signal_w(int, prefix + ".STOP")
|
|
80
84
|
# Whether set() should complete successfully or not
|
|
81
85
|
self._set_success = True
|
|
82
86
|
|
|
@@ -91,8 +95,8 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
91
95
|
|
|
92
96
|
super().__init__(name=name)
|
|
93
97
|
|
|
94
|
-
def set_name(self, name: str):
|
|
95
|
-
super().set_name(name)
|
|
98
|
+
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
99
|
+
super().set_name(name, child_name_separator=child_name_separator)
|
|
96
100
|
# Readback should be named the same as its parent in read()
|
|
97
101
|
self.user_readback.set_name(name)
|
|
98
102
|
|
|
@@ -122,9 +126,9 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
122
126
|
@AsyncStatus.wrap
|
|
123
127
|
async def kickoff(self):
|
|
124
128
|
"""Begin moving motor from prepared position to final position."""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
if not self._fly_completed_position:
|
|
130
|
+
msg = "Motor must be prepared before attempting to kickoff"
|
|
131
|
+
raise RuntimeError(msg)
|
|
128
132
|
|
|
129
133
|
self._fly_status = self.set(
|
|
130
134
|
self._fly_completed_position, timeout=self._fly_timeout
|
|
@@ -132,7 +136,9 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
132
136
|
|
|
133
137
|
def complete(self) -> WatchableAsyncStatus:
|
|
134
138
|
"""Mark as complete once motor reaches completed position."""
|
|
135
|
-
|
|
139
|
+
if not self._fly_status:
|
|
140
|
+
msg = "kickoff not called"
|
|
141
|
+
raise RuntimeError(msg)
|
|
136
142
|
return self._fly_status
|
|
137
143
|
|
|
138
144
|
@WatchableAsyncStatus.wrap
|
|
@@ -152,13 +158,18 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
152
158
|
self.velocity.get_value(),
|
|
153
159
|
self.acceleration_time.get_value(),
|
|
154
160
|
)
|
|
161
|
+
|
|
155
162
|
if timeout is CALCULATE_TIMEOUT:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
try:
|
|
164
|
+
timeout = (
|
|
165
|
+
abs((new_position - old_position) / velocity)
|
|
166
|
+
+ 2 * acceleration_time
|
|
167
|
+
+ DEFAULT_TIMEOUT
|
|
168
|
+
)
|
|
169
|
+
except ZeroDivisionError as error:
|
|
170
|
+
msg = "Mover has zero velocity"
|
|
171
|
+
raise ValueError(msg) from error
|
|
172
|
+
|
|
162
173
|
move_status = self.user_setpoint.set(new_position, wait=True, timeout=timeout)
|
|
163
174
|
async for current_position in observe_value(
|
|
164
175
|
self.user_readback, done_status=move_status
|
|
@@ -178,7 +189,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
178
189
|
self._set_success = success
|
|
179
190
|
# Put with completion will never complete as we are waiting for completion on
|
|
180
191
|
# the move above, so need to pass wait=False
|
|
181
|
-
await self.motor_stop.
|
|
192
|
+
await self.motor_stop.set(1, wait=False)
|
|
182
193
|
|
|
183
194
|
async def _prepare_velocity(
|
|
184
195
|
self, start_position: float, end_position: float, time_for_move: float
|