ophyd-async 0.9.0a2__py3-none-any.whl → 0.10.0a2__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/__init__.py +5 -8
- ophyd_async/_docs_parser.py +12 -0
- ophyd_async/_version.py +9 -4
- ophyd_async/core/__init__.py +97 -62
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +106 -125
- ophyd_async/core/_device.py +69 -63
- ophyd_async/core/_device_filler.py +65 -1
- ophyd_async/core/_flyer.py +14 -5
- ophyd_async/core/_hdf_dataset.py +29 -22
- ophyd_async/core/_log.py +14 -23
- ophyd_async/core/_mock_signal_backend.py +11 -3
- ophyd_async/core/_protocol.py +65 -45
- ophyd_async/core/_providers.py +28 -9
- ophyd_async/core/_readable.py +44 -35
- ophyd_async/core/_settings.py +36 -27
- ophyd_async/core/_signal.py +262 -170
- ophyd_async/core/_signal_backend.py +56 -13
- ophyd_async/core/_soft_signal_backend.py +16 -11
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +41 -11
- ophyd_async/core/_utils.py +96 -49
- ophyd_async/core/_yaml_settings.py +2 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/_andor.py +2 -2
- ophyd_async/epics/adandor/_andor_controller.py +4 -2
- ophyd_async/epics/adandor/_andor_io.py +2 -4
- ophyd_async/epics/adaravis/__init__.py +5 -0
- ophyd_async/epics/adaravis/_aravis.py +4 -8
- ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +23 -8
- ophyd_async/epics/adcore/_core_detector.py +42 -2
- ophyd_async/epics/adcore/_core_io.py +124 -99
- ophyd_async/epics/adcore/_core_logic.py +106 -27
- ophyd_async/epics/adcore/_core_writer.py +12 -8
- ophyd_async/epics/adcore/_hdf_writer.py +21 -38
- ophyd_async/epics/adcore/_single_trigger.py +2 -2
- ophyd_async/epics/adcore/_utils.py +2 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +3 -3
- ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +5 -0
- ophyd_async/epics/adpilatus/_pilatus.py +1 -1
- ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +4 -14
- ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +3 -2
- ophyd_async/epics/advimba/_vimba_controller.py +4 -2
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +35 -16
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +10 -2
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +4 -4
- ophyd_async/epics/demo/__init__.py +16 -0
- ophyd_async/epics/demo/__main__.py +31 -0
- ophyd_async/epics/demo/_ioc.py +32 -0
- ophyd_async/epics/demo/_motor.py +82 -0
- ophyd_async/epics/demo/_point_detector.py +42 -0
- ophyd_async/epics/demo/_point_detector_channel.py +22 -0
- ophyd_async/epics/demo/_stage.py +15 -0
- ophyd_async/epics/{sim/mover.db → demo/motor.db} +2 -1
- ophyd_async/epics/demo/point_detector.db +59 -0
- ophyd_async/epics/demo/point_detector_channel.db +21 -0
- ophyd_async/epics/eiger/_eiger.py +1 -3
- ophyd_async/epics/eiger/_eiger_controller.py +11 -4
- ophyd_async/epics/eiger/_eiger_io.py +2 -0
- ophyd_async/epics/eiger/_odin_io.py +1 -2
- ophyd_async/epics/motor.py +65 -28
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/_example_ioc.py +21 -9
- ophyd_async/epics/testing/_utils.py +3 -0
- ophyd_async/epics/testing/test_records.db +8 -0
- ophyd_async/epics/testing/test_records_pva.db +17 -16
- ophyd_async/fastcs/__init__.py +1 -0
- ophyd_async/fastcs/core.py +6 -0
- ophyd_async/fastcs/odin/__init__.py +1 -0
- ophyd_async/fastcs/panda/__init__.py +8 -6
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +5 -0
- ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
- ophyd_async/fastcs/panda/_table.py +9 -6
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +2 -0
- ophyd_async/plan_stubs/_ensure_connected.py +1 -0
- ophyd_async/plan_stubs/_fly.py +2 -4
- ophyd_async/plan_stubs/_nd_attributes.py +2 -0
- ophyd_async/plan_stubs/_panda.py +1 -0
- ophyd_async/plan_stubs/_settings.py +43 -16
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
- ophyd_async/sim/__init__.py +24 -14
- ophyd_async/sim/__main__.py +43 -0
- ophyd_async/sim/_blob_detector.py +33 -0
- ophyd_async/sim/_blob_detector_controller.py +48 -0
- ophyd_async/sim/_blob_detector_writer.py +105 -0
- ophyd_async/sim/_mirror_horizontal.py +46 -0
- ophyd_async/sim/_mirror_vertical.py +74 -0
- ophyd_async/sim/_motor.py +233 -0
- ophyd_async/sim/_pattern_generator.py +124 -0
- ophyd_async/sim/_point_detector.py +86 -0
- ophyd_async/sim/_stage.py +19 -0
- ophyd_async/tango/__init__.py +1 -0
- ophyd_async/tango/core/__init__.py +6 -1
- ophyd_async/tango/core/_base_device.py +41 -33
- ophyd_async/tango/core/_converters.py +81 -0
- ophyd_async/tango/core/_signal.py +18 -32
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +136 -60
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/{sim → demo}/_counter.py +2 -0
- ophyd_async/tango/{sim → demo}/_detector.py +2 -0
- ophyd_async/tango/{sim → demo}/_mover.py +5 -4
- ophyd_async/tango/{sim → demo}/_tango/_servers.py +4 -0
- ophyd_async/tango/testing/__init__.py +6 -0
- ophyd_async/tango/testing/_one_of_everything.py +200 -0
- ophyd_async/testing/__init__.py +29 -7
- ophyd_async/testing/_assert.py +145 -83
- ophyd_async/testing/_mock_signal_utils.py +56 -70
- ophyd_async/testing/_one_of_everything.py +41 -21
- ophyd_async/testing/_single_derived.py +89 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a2.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/WHEEL +1 -1
- ophyd_async/epics/sim/__init__.py +0 -54
- ophyd_async/epics/sim/_ioc.py +0 -29
- ophyd_async/epics/sim/_mover.py +0 -101
- ophyd_async/epics/sim/_sensor.py +0 -37
- ophyd_async/epics/sim/sensor.db +0 -19
- ophyd_async/sim/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
- ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
- ophyd_async/sim/_sim_motor.py +0 -107
- ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
- /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
- /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Annotated as A
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import SignalR, SignalRW, StandardReadable, StrictEnum
|
|
4
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
5
|
+
from ophyd_async.epics.core import EpicsDevice, PvSuffix
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EnergyMode(StrictEnum):
|
|
9
|
+
"""Energy mode for `DemoPointDetectorChannel`."""
|
|
10
|
+
|
|
11
|
+
LOW = "Low Energy"
|
|
12
|
+
"""Low energy mode"""
|
|
13
|
+
|
|
14
|
+
HIGH = "High Energy"
|
|
15
|
+
"""High energy mode"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DemoPointDetectorChannel(StandardReadable, EpicsDevice):
|
|
19
|
+
"""A channel for `DemoPointDetector` with int value based on X and Y Motors."""
|
|
20
|
+
|
|
21
|
+
value: A[SignalR[int], PvSuffix("Value"), Format.HINTED_UNCACHED_SIGNAL]
|
|
22
|
+
mode: A[SignalRW[EnergyMode], PvSuffix("Mode"), Format.CONFIG_SIGNAL]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from ophyd_async.core import StandardReadable
|
|
2
|
+
|
|
3
|
+
from ._motor import DemoMotor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DemoStage(StandardReadable):
|
|
7
|
+
"""A simulated sample stage with X and Y movables."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, prefix: str, name="") -> None:
|
|
10
|
+
# Define some child Devices
|
|
11
|
+
with self.add_children_as_readables():
|
|
12
|
+
self.x = DemoMotor(prefix + "X:")
|
|
13
|
+
self.y = DemoMotor(prefix + "Y:")
|
|
14
|
+
# Set name of device and child devices
|
|
15
|
+
super().__init__(name=name)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
record(ao, "$(P)AcquireTime") {
|
|
2
|
+
field(DESC, "Time to acquire for")
|
|
3
|
+
field(VAL, "0.1")
|
|
4
|
+
field(OUT, "$(P)Start.DLY2")
|
|
5
|
+
field(PINI, "YES")
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
record(seq, "$(P)Start") {
|
|
9
|
+
field(DESC, "Start sequence")
|
|
10
|
+
# Grab the start time
|
|
11
|
+
field(LNK0, "$(P)StartTime.PROC")
|
|
12
|
+
# Set it to be acquiring
|
|
13
|
+
field(LNK1, "$(P)Acquiring PP")
|
|
14
|
+
field(DO1, "1")
|
|
15
|
+
# Set it back to idle
|
|
16
|
+
field(LNK2, "$(P)Acquiring PP")
|
|
17
|
+
field(DO2, "0")
|
|
18
|
+
# Set the elapsed time to the full acquire time
|
|
19
|
+
field(LNK3, "$(P)Elapsed PP")
|
|
20
|
+
field(DOL3, "$(P)AcquireTime")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
record(ai, "$(P)StartTime") {
|
|
24
|
+
field(DTYP, "Soft Timestamp")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
record(bi, "$(P)Acquiring") {
|
|
28
|
+
field(DESC, "Currently acquiring")
|
|
29
|
+
field(ZNAM, "Idle")
|
|
30
|
+
field(ONAM, "Acquiring")
|
|
31
|
+
field(PINI, "YES")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
record(ai, "$(P)CurrentTime") {
|
|
35
|
+
field(DTYP, "Soft Timestamp")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
record(calcout, "$(P)Process") {
|
|
39
|
+
field(DESC, "Process elapsed time if acquiring")
|
|
40
|
+
field(INPA, "$(P)StartTime")
|
|
41
|
+
field(INPB, "$(P)CurrentTime PP")
|
|
42
|
+
field(SCAN, ".1 second")
|
|
43
|
+
field(CALC, "B-A")
|
|
44
|
+
field(OUT, "$(P)Elapsed PP")
|
|
45
|
+
field(SDIS, "$(P)Acquiring")
|
|
46
|
+
field(DISV, "0")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
record(ai, "$(P)Elapsed") {
|
|
50
|
+
field(DESC, "Elapsed time")
|
|
51
|
+
field(EGU, "s")
|
|
52
|
+
field(PREC, "1")
|
|
53
|
+
field(PINI, "YES")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
record(calcout, "$(P)Reset") {
|
|
57
|
+
field(OUT, "$(P)Elapsed PP")
|
|
58
|
+
field(CALC, "0")
|
|
59
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
record(mbbo, "$(P)$(CHANNEL):Mode") {
|
|
2
|
+
field(DESC, "Energy sensitivity of the image")
|
|
3
|
+
field(DTYP, "Raw Soft Channel")
|
|
4
|
+
field(PINI, "YES")
|
|
5
|
+
field(ZRVL, "10")
|
|
6
|
+
field(ZRST, "Low Energy")
|
|
7
|
+
field(ONVL, "100")
|
|
8
|
+
field(ONST, "High Energy")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
record(calc, "$(P)$(CHANNEL):Value") {
|
|
12
|
+
field(DESC, "Sensor value simulated from X and Y")
|
|
13
|
+
field(INPA, "$(X)Readback")
|
|
14
|
+
field(INPB, "$(Y)Readback")
|
|
15
|
+
field(INPC, "$(CHANNEL)")
|
|
16
|
+
field(INPD, "$(P)$(CHANNEL):Mode.RVAL")
|
|
17
|
+
field(INPE, "$(P)Elapsed CP")
|
|
18
|
+
field(CALC, "FLOOR((SIN(A)**C+COS(A*B+D)+2)*2500*E)")
|
|
19
|
+
field(EGU, "cts")
|
|
20
|
+
field(PREC, "0")
|
|
21
|
+
}
|
|
@@ -12,9 +12,7 @@ class EigerTriggerInfo(TriggerInfo):
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class EigerDetector(StandardDetector):
|
|
15
|
-
"""
|
|
16
|
-
Ophyd-async implementation of an Eiger Detector.
|
|
17
|
-
"""
|
|
15
|
+
"""Ophyd-async implementation of an Eiger Detector."""
|
|
18
16
|
|
|
19
17
|
_controller: EigerController
|
|
20
18
|
_writer: Odin
|
|
@@ -2,6 +2,7 @@ import asyncio
|
|
|
2
2
|
|
|
3
3
|
from ophyd_async.core import (
|
|
4
4
|
DEFAULT_TIMEOUT,
|
|
5
|
+
AsyncStatus,
|
|
5
6
|
DetectorController,
|
|
6
7
|
DetectorTrigger,
|
|
7
8
|
TriggerInfo,
|
|
@@ -19,19 +20,24 @@ EIGER_TRIGGER_MODE_MAP = {
|
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class EigerController(DetectorController):
|
|
23
|
+
"""Controller for the Eiger detector."""
|
|
24
|
+
|
|
22
25
|
def __init__(
|
|
23
26
|
self,
|
|
24
27
|
driver: EigerDriverIO,
|
|
25
28
|
) -> None:
|
|
26
29
|
self._drv = driver
|
|
30
|
+
self._arm_status: AsyncStatus | None = None
|
|
27
31
|
|
|
28
32
|
def get_deadtime(self, exposure: float | None) -> float:
|
|
29
33
|
# See https://media.dectris.com/filer_public/30/14/3014704e-5f3b-43ba-8ccf-8ef720e60d2a/240202_usermanual_eiger2.pdf
|
|
30
34
|
return 0.0001
|
|
31
35
|
|
|
32
36
|
async def set_energy(self, energy: float, tolerance: float = 0.1):
|
|
33
|
-
"""
|
|
34
|
-
|
|
37
|
+
"""Change photon energy if outside tolerance.
|
|
38
|
+
|
|
39
|
+
It takes some time so don't do it unless it is outside tolerance.
|
|
40
|
+
"""
|
|
35
41
|
current_energy = await self._drv.photon_energy.get_value()
|
|
36
42
|
if abs(current_energy - energy) > tolerance:
|
|
37
43
|
await self._drv.photon_energy.set(energy)
|
|
@@ -54,7 +60,7 @@ class EigerController(DetectorController):
|
|
|
54
60
|
|
|
55
61
|
async def arm(self):
|
|
56
62
|
# TODO: Detector state should be an enum see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
|
|
57
|
-
self._arm_status = set_and_wait_for_other_value(
|
|
63
|
+
self._arm_status = await set_and_wait_for_other_value(
|
|
58
64
|
self._drv.arm,
|
|
59
65
|
1,
|
|
60
66
|
self._drv.state,
|
|
@@ -64,8 +70,9 @@ class EigerController(DetectorController):
|
|
|
64
70
|
)
|
|
65
71
|
|
|
66
72
|
async def wait_for_idle(self):
|
|
67
|
-
if self._arm_status:
|
|
73
|
+
if self._arm_status and not self._arm_status.done:
|
|
68
74
|
await self._arm_status
|
|
75
|
+
self._arm_status = None
|
|
69
76
|
|
|
70
77
|
async def disarm(self):
|
|
71
78
|
await self._drv.disarm.set(1)
|
|
@@ -9,6 +9,8 @@ class EigerTriggerMode(StrictEnum):
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class EigerDriverIO(Device):
|
|
12
|
+
"""Contains signals for handling IO on the Eiger detector."""
|
|
13
|
+
|
|
12
14
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
13
15
|
self.bit_depth = epics_signal_r(int, f"{prefix}BitDepthReadout")
|
|
14
16
|
self.stale_parameters = epics_signal_r(bool, f"{prefix}StaleParameters")
|
|
@@ -5,7 +5,6 @@ from bluesky.protocols import StreamAsset
|
|
|
5
5
|
from event_model import DataKey
|
|
6
6
|
|
|
7
7
|
from ophyd_async.core import (
|
|
8
|
-
DEFAULT_TIMEOUT,
|
|
9
8
|
DetectorWriter,
|
|
10
9
|
Device,
|
|
11
10
|
DeviceVector,
|
|
@@ -110,7 +109,7 @@ class OdinWriter(DetectorWriter):
|
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
async def observe_indices_written(
|
|
113
|
-
self, timeout
|
|
112
|
+
self, timeout: float
|
|
114
113
|
) -> AsyncGenerator[int, None]:
|
|
115
114
|
async for num_captured in observe_value(self._drv.num_captured, timeout):
|
|
116
115
|
yield num_captured
|
ophyd_async/epics/motor.py
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
"""Support for EPICS motor record.
|
|
2
|
+
|
|
3
|
+
https://github.com/epics-modules/motor
|
|
4
|
+
"""
|
|
5
|
+
|
|
1
6
|
import asyncio
|
|
2
7
|
|
|
3
8
|
from bluesky.protocols import (
|
|
@@ -5,7 +10,9 @@ from bluesky.protocols import (
|
|
|
5
10
|
Locatable,
|
|
6
11
|
Location,
|
|
7
12
|
Preparable,
|
|
13
|
+
Reading,
|
|
8
14
|
Stoppable,
|
|
15
|
+
Subscribable,
|
|
9
16
|
)
|
|
10
17
|
from pydantic import BaseModel, Field
|
|
11
18
|
|
|
@@ -14,7 +21,9 @@ from ophyd_async.core import (
|
|
|
14
21
|
DEFAULT_TIMEOUT,
|
|
15
22
|
AsyncStatus,
|
|
16
23
|
CalculatableTimeout,
|
|
24
|
+
Callback,
|
|
17
25
|
StandardReadable,
|
|
26
|
+
StrictEnum,
|
|
18
27
|
WatchableAsyncStatus,
|
|
19
28
|
WatcherUpdate,
|
|
20
29
|
observe_value,
|
|
@@ -22,41 +31,54 @@ from ophyd_async.core import (
|
|
|
22
31
|
from ophyd_async.core import StandardReadableFormat as Format
|
|
23
32
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
|
|
24
33
|
|
|
34
|
+
__all__ = ["MotorLimitsException", "FlyMotorInfo", "Motor"]
|
|
25
35
|
|
|
26
|
-
class MotorLimitsException(Exception):
|
|
27
|
-
pass
|
|
28
36
|
|
|
37
|
+
class MotorLimitsException(Exception):
|
|
38
|
+
"""Exception for invalid motor limits."""
|
|
29
39
|
|
|
30
|
-
class InvalidFlyMotorException(Exception):
|
|
31
40
|
pass
|
|
32
41
|
|
|
33
42
|
|
|
34
|
-
DEFAULT_MOTOR_FLY_TIMEOUT = 60
|
|
35
|
-
DEFAULT_WATCHER_UPDATE_FREQUENCY = 0.2
|
|
36
|
-
|
|
37
|
-
|
|
38
43
|
class FlyMotorInfo(BaseModel):
|
|
39
|
-
"""Minimal set of information required to fly a motor
|
|
44
|
+
"""Minimal set of information required to fly a motor."""
|
|
40
45
|
|
|
41
|
-
#: Absolute position of the motor once it finishes accelerating to desired
|
|
42
|
-
#: velocity, in motor EGUs
|
|
43
46
|
start_position: float = Field(frozen=True)
|
|
47
|
+
"""Absolute position of the motor once it finishes accelerating to desired
|
|
48
|
+
velocity, in motor EGUs"""
|
|
44
49
|
|
|
45
|
-
#: Absolute position of the motor once it begins decelerating from desired
|
|
46
|
-
#: velocity, in EGUs
|
|
47
50
|
end_position: float = Field(frozen=True)
|
|
51
|
+
"""Absolute position of the motor once it begins decelerating from desired
|
|
52
|
+
velocity, in EGUs"""
|
|
48
53
|
|
|
49
|
-
#: Time taken for the motor to get from start_position to end_position, excluding
|
|
50
|
-
#: run-up and run-down, in seconds.
|
|
51
54
|
time_for_move: float = Field(frozen=True, gt=0)
|
|
55
|
+
"""Time taken for the motor to get from start_position to end_position, excluding
|
|
56
|
+
run-up and run-down, in seconds."""
|
|
52
57
|
|
|
53
|
-
#: Maximum time for the complete motor move, including run up and run down.
|
|
54
|
-
#: Defaults to `time_for_move` + run up and run down times + 10s.
|
|
55
58
|
timeout: CalculatableTimeout = Field(frozen=True, default=CALCULATE_TIMEOUT)
|
|
59
|
+
"""Maximum time for the complete motor move, including run up and run down.
|
|
60
|
+
Defaults to `time_for_move` + run up and run down times + 10s."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OffsetMode(StrictEnum):
|
|
64
|
+
VARIABLE = "Variable"
|
|
65
|
+
FROZEN = "Frozen"
|
|
66
|
+
|
|
56
67
|
|
|
68
|
+
class UseSetMode(StrictEnum):
|
|
69
|
+
USE = "Use"
|
|
70
|
+
SET = "Set"
|
|
57
71
|
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
|
|
73
|
+
class Motor(
|
|
74
|
+
StandardReadable,
|
|
75
|
+
Locatable[float],
|
|
76
|
+
Stoppable,
|
|
77
|
+
Flyable,
|
|
78
|
+
Preparable,
|
|
79
|
+
Subscribable[float],
|
|
80
|
+
):
|
|
81
|
+
"""Device that moves a motor record."""
|
|
60
82
|
|
|
61
83
|
def __init__(self, prefix: str, name="") -> None:
|
|
62
84
|
# Define some signals
|
|
@@ -76,11 +98,16 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
76
98
|
self.motor_done_move = epics_signal_r(int, prefix + ".DMOV")
|
|
77
99
|
self.low_limit_travel = epics_signal_rw(float, prefix + ".LLM")
|
|
78
100
|
self.high_limit_travel = epics_signal_rw(float, prefix + ".HLM")
|
|
101
|
+
self.offset_freeze_switch = epics_signal_rw(OffsetMode, prefix + ".FOFF")
|
|
102
|
+
self.high_limit_switch = epics_signal_r(int, prefix + ".HLS")
|
|
103
|
+
self.low_limit_switch = epics_signal_r(int, prefix + ".LLS")
|
|
104
|
+
self.set_use_switch = epics_signal_rw(UseSetMode, prefix + ".SET")
|
|
79
105
|
|
|
80
106
|
# Note:cannot use epics_signal_x here, as the motor record specifies that
|
|
81
107
|
# we must write 1 to stop the motor. Simply processing the record is not
|
|
82
108
|
# sufficient.
|
|
83
109
|
self.motor_stop = epics_signal_w(int, prefix + ".STOP")
|
|
110
|
+
|
|
84
111
|
# Whether set() should complete successfully or not
|
|
85
112
|
self._set_success = True
|
|
86
113
|
|
|
@@ -96,15 +123,14 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
96
123
|
super().__init__(name=name)
|
|
97
124
|
|
|
98
125
|
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
126
|
+
"""Set name of the motor and its children."""
|
|
99
127
|
super().set_name(name, child_name_separator=child_name_separator)
|
|
100
128
|
# Readback should be named the same as its parent in read()
|
|
101
129
|
self.user_readback.set_name(name)
|
|
102
130
|
|
|
103
131
|
@AsyncStatus.wrap
|
|
104
132
|
async def prepare(self, value: FlyMotorInfo):
|
|
105
|
-
"""
|
|
106
|
-
breached, move to start position minus run-up distance"""
|
|
107
|
-
|
|
133
|
+
"""Move to the beginning of a suitable run-up distance ready for a flyscan."""
|
|
108
134
|
self._fly_timeout = value.timeout
|
|
109
135
|
|
|
110
136
|
# Velocity, at which motor travels from start_position to end_position, in motor
|
|
@@ -142,8 +168,10 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
142
168
|
return self._fly_status
|
|
143
169
|
|
|
144
170
|
@WatchableAsyncStatus.wrap
|
|
145
|
-
async def set(
|
|
146
|
-
new_position =
|
|
171
|
+
async def set( # type: ignore
|
|
172
|
+
self, new_position: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
|
|
173
|
+
):
|
|
174
|
+
"""Move motor to the given value."""
|
|
147
175
|
self._set_success = True
|
|
148
176
|
(
|
|
149
177
|
old_position,
|
|
@@ -186,6 +214,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
186
214
|
raise RuntimeError("Motor was stopped")
|
|
187
215
|
|
|
188
216
|
async def stop(self, success=False):
|
|
217
|
+
"""Request to stop moving and return immediately."""
|
|
189
218
|
self._set_success = success
|
|
190
219
|
# Put with completion will never complete as we are waiting for completion on
|
|
191
220
|
# the move above, so need to pass wait=False
|
|
@@ -208,11 +237,19 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
208
237
|
return fly_velocity
|
|
209
238
|
|
|
210
239
|
async def locate(self) -> Location[float]:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return
|
|
240
|
+
"""Return the current setpoint and readback of the motor."""
|
|
241
|
+
setpoint, readback = await asyncio.gather(
|
|
242
|
+
self.user_setpoint.get_value(), self.user_readback.get_value()
|
|
243
|
+
)
|
|
244
|
+
return Location(setpoint=setpoint, readback=readback)
|
|
245
|
+
|
|
246
|
+
def subscribe(self, function: Callback[dict[str, Reading[float]]]) -> None:
|
|
247
|
+
"""Subscribe."""
|
|
248
|
+
self.user_readback.subscribe(function)
|
|
249
|
+
|
|
250
|
+
def clear_sub(self, function: Callback[dict[str, Reading[float]]]) -> None:
|
|
251
|
+
"""Unsubscribe."""
|
|
252
|
+
self.user_readback.clear_sub(function)
|
|
216
253
|
|
|
217
254
|
async def _prepare_motor_path(
|
|
218
255
|
self, fly_velocity: float, start_position: float, end_position: float
|
ophyd_async/epics/signal.py
CHANGED
|
@@ -15,35 +15,43 @@ PVA_RECORDS = Path(__file__).parent / "test_records_pva.db"
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class EpicsTestEnum(StrictEnum):
|
|
18
|
+
"""For testing strict enum values in test IOCs."""
|
|
19
|
+
|
|
18
20
|
A = "Aaa"
|
|
19
21
|
B = "Bbb"
|
|
20
22
|
C = "Ccc"
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
class EpicsTestSubsetEnum(SubsetEnum):
|
|
26
|
+
"""For testing subset enum values in test IOCs."""
|
|
27
|
+
|
|
24
28
|
A = "Aaa"
|
|
25
29
|
B = "Bbb"
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
class EpicsTestTable(Table):
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
a_bool: Array1D[np.bool_]
|
|
34
|
+
a_int: Array1D[np.int32]
|
|
35
|
+
a_float: Array1D[np.float64]
|
|
36
|
+
a_str: Sequence[str]
|
|
37
|
+
a_enum: Sequence[EpicsTestEnum]
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
class EpicsTestCaDevice(EpicsDevice):
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
"""Device for use in a channel access test IOC."""
|
|
42
|
+
|
|
43
|
+
a_int: A[SignalRW[int], PvSuffix("int")]
|
|
44
|
+
"""A thing"""
|
|
45
|
+
a_float: A[SignalRW[float], PvSuffix("float")]
|
|
39
46
|
float_prec_0: A[SignalRW[int], PvSuffix("float_prec_0")]
|
|
40
|
-
|
|
47
|
+
a_str: A[SignalRW[str], PvSuffix("str")]
|
|
41
48
|
longstr: A[SignalRW[str], PvSuffix("longstr")]
|
|
42
49
|
longstr2: A[SignalRW[str], PvSuffix("longstr2.VAL$")]
|
|
43
|
-
|
|
50
|
+
a_bool: A[SignalRW[bool], PvSuffix("bool")]
|
|
44
51
|
enum: A[SignalRW[EpicsTestEnum], PvSuffix("enum")]
|
|
45
52
|
enum2: A[SignalRW[EpicsTestEnum], PvSuffix("enum2")]
|
|
46
53
|
subset_enum: A[SignalRW[EpicsTestSubsetEnum], PvSuffix("subset_enum")]
|
|
54
|
+
enum_str_fallback: A[SignalRW[str], PvSuffix("enum_str_fallback")]
|
|
47
55
|
bool_unnamed: A[SignalRW[bool], PvSuffix("bool_unnamed")]
|
|
48
56
|
partialint: A[SignalRW[int], PvSuffix("partialint")]
|
|
49
57
|
lessint: A[SignalRW[int], PvSuffix("lessint")]
|
|
@@ -56,6 +64,8 @@ class EpicsTestCaDevice(EpicsDevice):
|
|
|
56
64
|
|
|
57
65
|
|
|
58
66
|
class EpicsTestPvaDevice(EpicsTestCaDevice):
|
|
67
|
+
"""Device for use in a pv access test IOC."""
|
|
68
|
+
|
|
59
69
|
# pva can support all signal types that ca can
|
|
60
70
|
int8a: A[SignalRW[Array1D[np.int8]], PvSuffix("int8a")]
|
|
61
71
|
uint16a: A[SignalRW[Array1D[np.uint16]], PvSuffix("uint16a")]
|
|
@@ -67,6 +77,8 @@ class EpicsTestPvaDevice(EpicsTestCaDevice):
|
|
|
67
77
|
|
|
68
78
|
|
|
69
79
|
class EpicsTestIocAndDevices:
|
|
80
|
+
"""Test IOC with ca and pva devices."""
|
|
81
|
+
|
|
70
82
|
def __init__(self):
|
|
71
83
|
self.prefix = generate_random_pv_prefix()
|
|
72
84
|
self.ioc = TestingIOC()
|
|
@@ -7,10 +7,13 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def generate_random_pv_prefix() -> str:
|
|
10
|
+
"""For generating random PV names in test devices."""
|
|
10
11
|
return "".join(random.choice(string.ascii_lowercase) for _ in range(12)) + ":"
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class TestingIOC:
|
|
15
|
+
"""For initialising an IOC in tests."""
|
|
16
|
+
|
|
14
17
|
def __init__(self):
|
|
15
18
|
self._db_macros: list[tuple[Path, dict[str, str]]] = []
|
|
16
19
|
self.output = ""
|
|
@@ -104,6 +104,14 @@ record(mbbo, "$(device)subset_enum") {
|
|
|
104
104
|
field(PINI, "YES")
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
record(mbbo, "$(device)enum_str_fallback") {
|
|
108
|
+
field(ZRST, "Aaa")
|
|
109
|
+
field(ONST, "Bbb")
|
|
110
|
+
field(TWST, "Ccc")
|
|
111
|
+
field(VAL, "1")
|
|
112
|
+
field(PINI, "YES")
|
|
113
|
+
}
|
|
114
|
+
|
|
107
115
|
record(waveform, "$(device)uint8a") {
|
|
108
116
|
field(NELM, "3")
|
|
109
117
|
field(FTVL, "UCHAR")
|
|
@@ -1,36 +1,37 @@
|
|
|
1
1
|
record(waveform, "$(device)int8a") {
|
|
2
|
-
field(NELM, "
|
|
2
|
+
field(NELM, "7")
|
|
3
3
|
field(FTVL, "CHAR")
|
|
4
|
-
field(INP, {const:[-128, 127]})
|
|
4
|
+
field(INP, {const:[-128, 127, 0, 1, 2, 3, 4]})
|
|
5
5
|
field(PINI, "YES")
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
record(waveform, "$(device)uint16a") {
|
|
9
|
-
field(NELM, "
|
|
9
|
+
field(NELM, "7")
|
|
10
10
|
field(FTVL, "USHORT")
|
|
11
|
-
field(INP, {const:[0, 65535]})
|
|
11
|
+
field(INP, {const:[0, 65535, 0, 1, 2, 3, 4]})
|
|
12
12
|
field(PINI, "YES")
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
record(waveform, "$(device)uint32a") {
|
|
16
|
-
field(NELM, "
|
|
16
|
+
field(NELM, "7")
|
|
17
17
|
field(FTVL, "ULONG")
|
|
18
|
-
field(INP, {const:[0, 4294967295]})
|
|
18
|
+
field(INP, {const:[0, 4294967295, 0, 1, 2, 3, 4]})
|
|
19
19
|
field(PINI, "YES")
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
record(waveform, "$(device)int64a") {
|
|
23
|
-
field(NELM, "
|
|
23
|
+
field(NELM, "7")
|
|
24
24
|
field(FTVL, "INT64")
|
|
25
|
-
#
|
|
26
|
-
field(INP, {const:[-
|
|
25
|
+
# limit of range appears to be +/-(2^63 - 1)
|
|
26
|
+
field(INP, {const:[-9223372036854775807, 9223372036854775807, 0, 1, 2, 3, 4]})
|
|
27
27
|
field(PINI, "YES")
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
record(waveform, "$(device)uint64a") {
|
|
31
|
-
field(NELM, "
|
|
31
|
+
field(NELM, "7")
|
|
32
32
|
field(FTVL, "UINT64")
|
|
33
|
-
|
|
33
|
+
# limit of range appears to be 0 to +(2^63 - 1)
|
|
34
|
+
field(INP, {const:[0, 9223372036854775807, 0, 1, 2, 3, 4]})
|
|
34
35
|
field(PINI, "YES")
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -58,7 +59,7 @@ record(waveform, "$(device)table:bool")
|
|
|
58
59
|
field(PINI, "YES")
|
|
59
60
|
info(Q:group, {
|
|
60
61
|
"$(device)table": {
|
|
61
|
-
"value.
|
|
62
|
+
"value.a_bool": {
|
|
62
63
|
"+type": "plain",
|
|
63
64
|
"+channel": "VAL",
|
|
64
65
|
"+putorder": 1
|
|
@@ -75,7 +76,7 @@ record(waveform, "$(device)table:int")
|
|
|
75
76
|
field(PINI, "YES")
|
|
76
77
|
info(Q:group, {
|
|
77
78
|
"$(device)table": {
|
|
78
|
-
"value.
|
|
79
|
+
"value.a_int": {
|
|
79
80
|
"+type": "plain",
|
|
80
81
|
"+channel": "VAL",
|
|
81
82
|
"+putorder": 2
|
|
@@ -92,7 +93,7 @@ record(waveform, "$(device)table:float")
|
|
|
92
93
|
field(PINI, "YES")
|
|
93
94
|
info(Q:group, {
|
|
94
95
|
"$(device)table": {
|
|
95
|
-
"value.
|
|
96
|
+
"value.a_float": {
|
|
96
97
|
"+type": "plain",
|
|
97
98
|
"+channel": "VAL",
|
|
98
99
|
"+putorder": 3
|
|
@@ -109,7 +110,7 @@ record(waveform, "$(device)table:str")
|
|
|
109
110
|
field(PINI, "YES")
|
|
110
111
|
info(Q:group, {
|
|
111
112
|
"$(device)table": {
|
|
112
|
-
"value.
|
|
113
|
+
"value.a_str": {
|
|
113
114
|
"+type": "plain",
|
|
114
115
|
"+channel": "VAL",
|
|
115
116
|
"+putorder": 4
|
|
@@ -126,7 +127,7 @@ record(waveform, "$(device)table:enum")
|
|
|
126
127
|
field(PINI, "YES")
|
|
127
128
|
info(Q:group, {
|
|
128
129
|
"$(device)table": {
|
|
129
|
-
"value.
|
|
130
|
+
"value.a_enum": {
|
|
130
131
|
"+type": "plain",
|
|
131
132
|
"+channel": "VAL",
|
|
132
133
|
"+putorder": 5,
|
ophyd_async/fastcs/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FastCS support for Signals via EPICS or Tango, and Devices that use them."""
|
ophyd_async/fastcs/core.py
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
"""FastCS core module for ophyd-async."""
|
|
2
|
+
|
|
1
3
|
from ophyd_async.core import Device, DeviceConnector
|
|
2
4
|
from ophyd_async.epics.core import PviDeviceConnector
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
def fastcs_connector(device: Device, uri: str, error_hint: str = "") -> DeviceConnector:
|
|
8
|
+
"""Create devices and connections on pvi device `Device`."""
|
|
6
9
|
# TODO: add Tango support based on uri scheme
|
|
7
10
|
connector = PviDeviceConnector(uri, error_hint)
|
|
8
11
|
connector.create_children_from_annotations(device)
|
|
9
12
|
return connector
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ["fastcs_connector"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = []
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from ._block import (
|
|
2
|
-
BitMux,
|
|
3
2
|
CommonPandaBlocks,
|
|
4
3
|
DataBlock,
|
|
4
|
+
PandaBitMux,
|
|
5
|
+
PandaCaptureMode,
|
|
6
|
+
PandaPcompDirection,
|
|
7
|
+
PandaTimeUnits,
|
|
5
8
|
PcapBlock,
|
|
6
9
|
PcompBlock,
|
|
7
|
-
PcompDirection,
|
|
8
10
|
PulseBlock,
|
|
9
11
|
SeqBlock,
|
|
10
|
-
TimeUnits,
|
|
11
12
|
)
|
|
12
13
|
from ._control import PandaPcapController
|
|
13
14
|
from ._hdf_panda import HDFPanda
|
|
@@ -28,13 +29,14 @@ from ._writer import PandaHDFWriter
|
|
|
28
29
|
__all__ = [
|
|
29
30
|
"CommonPandaBlocks",
|
|
30
31
|
"DataBlock",
|
|
31
|
-
"
|
|
32
|
+
"PandaBitMux",
|
|
33
|
+
"PandaCaptureMode",
|
|
32
34
|
"PcapBlock",
|
|
33
35
|
"PcompBlock",
|
|
34
|
-
"
|
|
36
|
+
"PandaPcompDirection",
|
|
35
37
|
"PulseBlock",
|
|
36
38
|
"SeqBlock",
|
|
37
|
-
"
|
|
39
|
+
"PandaTimeUnits",
|
|
38
40
|
"HDFPanda",
|
|
39
41
|
"PandaHDFWriter",
|
|
40
42
|
"PandaPcapController",
|