ophyd-async 0.7.0__py3-none-any.whl → 0.8.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 +34 -9
- ophyd_async/core/_detector.py +5 -10
- ophyd_async/core/_device.py +170 -68
- ophyd_async/core/_device_filler.py +269 -0
- ophyd_async/core/_device_save_loader.py +6 -7
- ophyd_async/core/_mock_signal_backend.py +35 -40
- ophyd_async/core/_mock_signal_utils.py +25 -16
- ophyd_async/core/_protocol.py +28 -8
- ophyd_async/core/_readable.py +133 -134
- ophyd_async/core/_signal.py +219 -163
- ophyd_async/core/_signal_backend.py +131 -64
- ophyd_async/core/_soft_signal_backend.py +131 -194
- ophyd_async/core/_status.py +22 -6
- ophyd_async/core/_table.py +102 -100
- ophyd_async/core/_utils.py +143 -32
- ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
- ophyd_async/epics/adaravis/_aravis_io.py +8 -6
- ophyd_async/epics/adcore/_core_io.py +5 -7
- ophyd_async/epics/adcore/_core_logic.py +3 -1
- ophyd_async/epics/adcore/_hdf_writer.py +2 -2
- ophyd_async/epics/adcore/_single_trigger.py +6 -10
- ophyd_async/epics/adcore/_utils.py +15 -10
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix_controller.py +6 -3
- ophyd_async/epics/adkinetix/_kinetix_io.py +4 -5
- ophyd_async/epics/adpilatus/_pilatus_controller.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus_io.py +3 -4
- ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
- ophyd_async/epics/advimba/__init__.py +4 -1
- ophyd_async/epics/advimba/_vimba_controller.py +6 -3
- ophyd_async/epics/advimba/_vimba_io.py +8 -9
- ophyd_async/epics/core/__init__.py +26 -0
- ophyd_async/epics/core/_aioca.py +323 -0
- ophyd_async/epics/core/_epics_connector.py +53 -0
- ophyd_async/epics/core/_epics_device.py +13 -0
- ophyd_async/epics/core/_p4p.py +383 -0
- ophyd_async/epics/core/_pvi_connector.py +91 -0
- ophyd_async/epics/core/_signal.py +171 -0
- ophyd_async/epics/core/_util.py +61 -0
- ophyd_async/epics/demo/_mover.py +4 -5
- ophyd_async/epics/demo/_sensor.py +14 -13
- ophyd_async/epics/eiger/_eiger.py +1 -2
- ophyd_async/epics/eiger/_eiger_controller.py +7 -2
- ophyd_async/epics/eiger/_eiger_io.py +3 -5
- ophyd_async/epics/eiger/_odin_io.py +5 -5
- ophyd_async/epics/motor.py +4 -5
- ophyd_async/epics/signal.py +11 -0
- ophyd_async/epics/testing/__init__.py +24 -0
- ophyd_async/epics/testing/_example_ioc.py +105 -0
- ophyd_async/epics/testing/_utils.py +78 -0
- ophyd_async/epics/testing/test_records.db +152 -0
- ophyd_async/epics/testing/test_records_pva.db +177 -0
- ophyd_async/fastcs/core.py +9 -0
- ophyd_async/fastcs/panda/__init__.py +4 -4
- ophyd_async/fastcs/panda/_block.py +18 -13
- ophyd_async/fastcs/panda/_control.py +3 -5
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -19
- ophyd_async/fastcs/panda/_table.py +30 -52
- ophyd_async/fastcs/panda/_trigger.py +8 -8
- ophyd_async/fastcs/panda/_writer.py +2 -5
- ophyd_async/plan_stubs/_ensure_connected.py +20 -13
- ophyd_async/plan_stubs/_fly.py +2 -2
- ophyd_async/plan_stubs/_nd_attributes.py +5 -4
- ophyd_async/py.typed +0 -0
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +1 -2
- ophyd_async/sim/demo/_sim_motor.py +3 -4
- ophyd_async/tango/__init__.py +0 -45
- ophyd_async/tango/{signal → core}/__init__.py +9 -6
- ophyd_async/tango/core/_base_device.py +132 -0
- ophyd_async/tango/{signal → core}/_signal.py +42 -53
- ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
- ophyd_async/tango/{signal → core}/_tango_transport.py +38 -40
- ophyd_async/tango/demo/_counter.py +12 -23
- ophyd_async/tango/demo/_mover.py +13 -13
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/METADATA +52 -55
- ophyd_async-0.8.0.dist-info/RECORD +116 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/WHEEL +1 -1
- ophyd_async/epics/pvi/__init__.py +0 -3
- ophyd_async/epics/pvi/_pvi.py +0 -338
- ophyd_async/epics/signal/__init__.py +0 -21
- ophyd_async/epics/signal/_aioca.py +0 -378
- ophyd_async/epics/signal/_common.py +0 -57
- ophyd_async/epics/signal/_epics_transport.py +0 -34
- ophyd_async/epics/signal/_p4p.py +0 -518
- ophyd_async/epics/signal/_signal.py +0 -114
- ophyd_async/tango/base_devices/__init__.py +0 -4
- ophyd_async/tango/base_devices/_base_device.py +0 -225
- ophyd_async-0.7.0.dist-info/RECORD +0 -108
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import Any, get_args, get_origin
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from ophyd_async.core import (
|
|
7
|
+
SignalBackend,
|
|
8
|
+
SignalDatatypeT,
|
|
9
|
+
SubsetEnum,
|
|
10
|
+
get_dtype,
|
|
11
|
+
get_enum_cls,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_supported_values(
|
|
16
|
+
pv: str,
|
|
17
|
+
datatype: type,
|
|
18
|
+
pv_choices: Sequence[str],
|
|
19
|
+
) -> dict[str, str]:
|
|
20
|
+
enum_cls = get_enum_cls(datatype)
|
|
21
|
+
if not enum_cls:
|
|
22
|
+
raise TypeError(f"{datatype} is not an Enum")
|
|
23
|
+
choices = [v.value for v in enum_cls]
|
|
24
|
+
error_msg = f"{pv} has choices {pv_choices}, but {datatype} requested {choices} "
|
|
25
|
+
if issubclass(enum_cls, SubsetEnum):
|
|
26
|
+
if not set(choices).issubset(pv_choices):
|
|
27
|
+
raise TypeError(error_msg + "to be a subset of them.")
|
|
28
|
+
else:
|
|
29
|
+
if set(choices) != set(pv_choices):
|
|
30
|
+
raise TypeError(error_msg + "to be strictly equal to them.")
|
|
31
|
+
|
|
32
|
+
# Take order from the pv choices
|
|
33
|
+
supported_values = {x: x for x in pv_choices}
|
|
34
|
+
# But override those that we specify via the datatype
|
|
35
|
+
for v in enum_cls:
|
|
36
|
+
supported_values[v.value] = v
|
|
37
|
+
return supported_values
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_datatype(datatype: Any) -> str:
|
|
41
|
+
if get_origin(datatype) is np.ndarray and get_args(datatype)[0] == tuple[int]:
|
|
42
|
+
dtype = get_dtype(datatype)
|
|
43
|
+
return f"Array1D[np.{dtype.name}]"
|
|
44
|
+
elif get_origin(datatype) is Sequence:
|
|
45
|
+
return f"Sequence[{get_args(datatype)[0].__name__}]"
|
|
46
|
+
elif isinstance(datatype, type):
|
|
47
|
+
return datatype.__name__
|
|
48
|
+
else:
|
|
49
|
+
return str(datatype)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class EpicsSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
datatype: type[SignalDatatypeT] | None,
|
|
56
|
+
read_pv: str = "",
|
|
57
|
+
write_pv: str = "",
|
|
58
|
+
):
|
|
59
|
+
self.read_pv = read_pv
|
|
60
|
+
self.write_pv = write_pv
|
|
61
|
+
super().__init__(datatype)
|
ophyd_async/epics/demo/_mover.py
CHANGED
|
@@ -8,15 +8,14 @@ from ophyd_async.core import (
|
|
|
8
8
|
DEFAULT_TIMEOUT,
|
|
9
9
|
AsyncStatus,
|
|
10
10
|
CalculatableTimeout,
|
|
11
|
-
ConfigSignal,
|
|
12
11
|
Device,
|
|
13
|
-
HintedSignal,
|
|
14
12
|
StandardReadable,
|
|
15
13
|
WatchableAsyncStatus,
|
|
16
14
|
WatcherUpdate,
|
|
17
15
|
observe_value,
|
|
18
16
|
)
|
|
19
|
-
from ophyd_async.
|
|
17
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
18
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class Mover(StandardReadable, Movable, Stoppable):
|
|
@@ -24,9 +23,9 @@ class Mover(StandardReadable, Movable, Stoppable):
|
|
|
24
23
|
|
|
25
24
|
def __init__(self, prefix: str, name="") -> None:
|
|
26
25
|
# Define some signals
|
|
27
|
-
with self.add_children_as_readables(
|
|
26
|
+
with self.add_children_as_readables(Format.HINTED_SIGNAL):
|
|
28
27
|
self.readback = epics_signal_r(float, prefix + "Readback")
|
|
29
|
-
with self.add_children_as_readables(
|
|
28
|
+
with self.add_children_as_readables(Format.CONFIG_SIGNAL):
|
|
30
29
|
self.velocity = epics_signal_rw(float, prefix + "Velocity")
|
|
31
30
|
self.units = epics_signal_r(str, prefix + "Readback.EGU")
|
|
32
31
|
self.setpoint = epics_signal_rw(float, prefix + "Setpoint")
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import Annotated as A
|
|
2
2
|
|
|
3
|
-
from ophyd_async.core import
|
|
4
|
-
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
DeviceVector,
|
|
5
|
+
SignalR,
|
|
6
|
+
SignalRW,
|
|
7
|
+
StandardReadable,
|
|
8
|
+
StrictEnum,
|
|
9
|
+
)
|
|
10
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
11
|
+
from ophyd_async.epics.core import EpicsDevice, PvSuffix
|
|
5
12
|
|
|
6
13
|
|
|
7
|
-
class EnergyMode(
|
|
14
|
+
class EnergyMode(StrictEnum):
|
|
8
15
|
"""Energy mode for `Sensor`"""
|
|
9
16
|
|
|
10
17
|
#: Low energy mode
|
|
@@ -13,17 +20,11 @@ class EnergyMode(str, Enum):
|
|
|
13
20
|
high = "High Energy"
|
|
14
21
|
|
|
15
22
|
|
|
16
|
-
class Sensor(StandardReadable):
|
|
23
|
+
class Sensor(StandardReadable, EpicsDevice):
|
|
17
24
|
"""A demo sensor that produces a scalar value based on X and Y Movers"""
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
with self.add_children_as_readables(HintedSignal):
|
|
22
|
-
self.value = epics_signal_r(float, prefix + "Value")
|
|
23
|
-
with self.add_children_as_readables(ConfigSignal):
|
|
24
|
-
self.mode = epics_signal_rw(EnergyMode, prefix + "Mode")
|
|
25
|
-
|
|
26
|
-
super().__init__(name=name)
|
|
26
|
+
value: A[SignalR[float], PvSuffix("Value"), Format.HINTED_SIGNAL]
|
|
27
|
+
mode: A[SignalRW[EnergyMode], PvSuffix("Mode"), Format.CONFIG_SIGNAL]
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
class SensorGroup(StandardReadable):
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from pydantic import Field
|
|
2
2
|
|
|
3
|
-
from ophyd_async.core import AsyncStatus, PathProvider, StandardDetector
|
|
4
|
-
from ophyd_async.core._detector import TriggerInfo
|
|
3
|
+
from ophyd_async.core import AsyncStatus, PathProvider, StandardDetector, TriggerInfo
|
|
5
4
|
|
|
6
5
|
from ._eiger_controller import EigerController
|
|
7
6
|
from ._eiger_io import EigerDriverIO
|
|
@@ -4,9 +4,9 @@ from ophyd_async.core import (
|
|
|
4
4
|
DEFAULT_TIMEOUT,
|
|
5
5
|
DetectorController,
|
|
6
6
|
DetectorTrigger,
|
|
7
|
+
TriggerInfo,
|
|
7
8
|
set_and_wait_for_other_value,
|
|
8
9
|
)
|
|
9
|
-
from ophyd_async.core._detector import TriggerInfo
|
|
10
10
|
|
|
11
11
|
from ._eiger_io import EigerDriverIO, EigerTriggerMode
|
|
12
12
|
|
|
@@ -55,7 +55,12 @@ class EigerController(DetectorController):
|
|
|
55
55
|
async def arm(self):
|
|
56
56
|
# TODO: Detector state should be an enum see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
|
|
57
57
|
self._arm_status = set_and_wait_for_other_value(
|
|
58
|
-
self._drv.arm,
|
|
58
|
+
self._drv.arm,
|
|
59
|
+
1,
|
|
60
|
+
self._drv.state,
|
|
61
|
+
"ready",
|
|
62
|
+
timeout=DEFAULT_TIMEOUT,
|
|
63
|
+
wait_for_set_completion=False,
|
|
59
64
|
)
|
|
60
65
|
|
|
61
66
|
async def wait_for_idle(self):
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
from
|
|
1
|
+
from ophyd_async.core import Device, StrictEnum
|
|
2
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw_rbv, epics_signal_w
|
|
2
3
|
|
|
3
|
-
from ophyd_async.core import Device
|
|
4
|
-
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw_rbv, epics_signal_w
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
class EigerTriggerMode(str, Enum):
|
|
5
|
+
class EigerTriggerMode(StrictEnum):
|
|
8
6
|
internal = "ints"
|
|
9
7
|
edge = "exts"
|
|
10
8
|
gate = "exte"
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from collections.abc import AsyncGenerator, AsyncIterator
|
|
3
|
-
from enum import Enum
|
|
4
3
|
|
|
5
4
|
from bluesky.protocols import StreamAsset
|
|
6
5
|
from event_model import DataKey
|
|
@@ -12,17 +11,18 @@ from ophyd_async.core import (
|
|
|
12
11
|
DeviceVector,
|
|
13
12
|
NameProvider,
|
|
14
13
|
PathProvider,
|
|
14
|
+
StrictEnum,
|
|
15
15
|
observe_value,
|
|
16
16
|
set_and_wait_for_value,
|
|
17
17
|
)
|
|
18
|
-
from ophyd_async.epics.
|
|
18
|
+
from ophyd_async.epics.core import (
|
|
19
19
|
epics_signal_r,
|
|
20
20
|
epics_signal_rw,
|
|
21
21
|
epics_signal_rw_rbv,
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
class Writing(
|
|
25
|
+
class Writing(StrictEnum):
|
|
26
26
|
ON = "ON"
|
|
27
27
|
OFF = "OFF"
|
|
28
28
|
|
|
@@ -101,10 +101,10 @@ class OdinWriter(DetectorWriter):
|
|
|
101
101
|
return {
|
|
102
102
|
"data": DataKey(
|
|
103
103
|
source=self._drv.file_name.source,
|
|
104
|
-
shape=data_shape,
|
|
104
|
+
shape=list(data_shape),
|
|
105
105
|
dtype="array",
|
|
106
106
|
# TODO: Use correct type based on eiger https://github.com/bluesky/ophyd-async/issues/529
|
|
107
|
-
dtype_numpy="<u2",
|
|
107
|
+
dtype_numpy="<u2",
|
|
108
108
|
external="STREAM:",
|
|
109
109
|
)
|
|
110
110
|
}
|
ophyd_async/epics/motor.py
CHANGED
|
@@ -14,14 +14,13 @@ from ophyd_async.core import (
|
|
|
14
14
|
DEFAULT_TIMEOUT,
|
|
15
15
|
AsyncStatus,
|
|
16
16
|
CalculatableTimeout,
|
|
17
|
-
ConfigSignal,
|
|
18
|
-
HintedSignal,
|
|
19
17
|
StandardReadable,
|
|
20
18
|
WatchableAsyncStatus,
|
|
21
19
|
WatcherUpdate,
|
|
22
20
|
observe_value,
|
|
23
21
|
)
|
|
24
|
-
from ophyd_async.
|
|
22
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
23
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
class MotorLimitsException(Exception):
|
|
@@ -61,11 +60,11 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
61
60
|
|
|
62
61
|
def __init__(self, prefix: str, name="") -> None:
|
|
63
62
|
# Define some signals
|
|
64
|
-
with self.add_children_as_readables(
|
|
63
|
+
with self.add_children_as_readables(Format.CONFIG_SIGNAL):
|
|
65
64
|
self.motor_egu = epics_signal_r(str, prefix + ".EGU")
|
|
66
65
|
self.velocity = epics_signal_rw(float, prefix + ".VELO")
|
|
67
66
|
|
|
68
|
-
with self.add_children_as_readables(
|
|
67
|
+
with self.add_children_as_readables(Format.HINTED_SIGNAL):
|
|
69
68
|
self.user_readback = epics_signal_r(float, prefix + ".RBV")
|
|
70
69
|
|
|
71
70
|
self.user_setpoint = epics_signal_rw(float, prefix + ".VAL")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from ._example_ioc import (
|
|
2
|
+
CA_PVA_RECORDS,
|
|
3
|
+
PVA_RECORDS,
|
|
4
|
+
ExampleCaDevice,
|
|
5
|
+
ExampleEnum,
|
|
6
|
+
ExamplePvaDevice,
|
|
7
|
+
ExampleTable,
|
|
8
|
+
connect_example_device,
|
|
9
|
+
get_example_ioc,
|
|
10
|
+
)
|
|
11
|
+
from ._utils import TestingIOC, generate_random_PV_prefix
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CA_PVA_RECORDS",
|
|
15
|
+
"PVA_RECORDS",
|
|
16
|
+
"ExampleCaDevice",
|
|
17
|
+
"ExampleEnum",
|
|
18
|
+
"ExamplePvaDevice",
|
|
19
|
+
"ExampleTable",
|
|
20
|
+
"connect_example_device",
|
|
21
|
+
"get_example_ioc",
|
|
22
|
+
"TestingIOC",
|
|
23
|
+
"generate_random_PV_prefix",
|
|
24
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Annotated as A
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from ophyd_async.core import (
|
|
9
|
+
Array1D,
|
|
10
|
+
SignalRW,
|
|
11
|
+
StrictEnum,
|
|
12
|
+
Table,
|
|
13
|
+
)
|
|
14
|
+
from ophyd_async.epics.core import (
|
|
15
|
+
EpicsDevice,
|
|
16
|
+
PvSuffix,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from ._utils import TestingIOC
|
|
20
|
+
|
|
21
|
+
CA_PVA_RECORDS = str(Path(__file__).parent / "test_records.db")
|
|
22
|
+
PVA_RECORDS = str(Path(__file__).parent / "test_records_pva.db")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExampleEnum(StrictEnum):
|
|
26
|
+
a = "Aaa"
|
|
27
|
+
b = "Bbb"
|
|
28
|
+
c = "Ccc"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ExampleTable(Table):
|
|
32
|
+
bool: Array1D[np.bool_]
|
|
33
|
+
int: Array1D[np.int32]
|
|
34
|
+
float: Array1D[np.float64]
|
|
35
|
+
str: Sequence[str]
|
|
36
|
+
enum: Sequence[ExampleEnum]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ExampleCaDevice(EpicsDevice):
|
|
40
|
+
my_int: A[SignalRW[int], PvSuffix("int")]
|
|
41
|
+
my_float: A[SignalRW[float], PvSuffix("float")]
|
|
42
|
+
my_str: A[SignalRW[str], PvSuffix("str")]
|
|
43
|
+
my_bool: A[SignalRW[bool], PvSuffix("bool")]
|
|
44
|
+
enum: A[SignalRW[ExampleEnum], PvSuffix("enum")]
|
|
45
|
+
enum2: A[SignalRW[ExampleEnum], PvSuffix("enum2")]
|
|
46
|
+
bool_unnamed: A[SignalRW[bool], PvSuffix("bool_unnamed")]
|
|
47
|
+
partialint: A[SignalRW[int], PvSuffix("partialint")]
|
|
48
|
+
lessint: A[SignalRW[int], PvSuffix("lessint")]
|
|
49
|
+
uint8a: A[SignalRW[Array1D[np.uint8]], PvSuffix("uint8a")]
|
|
50
|
+
int16a: A[SignalRW[Array1D[np.int16]], PvSuffix("int16a")]
|
|
51
|
+
int32a: A[SignalRW[Array1D[np.int32]], PvSuffix("int32a")]
|
|
52
|
+
float32a: A[SignalRW[Array1D[np.float32]], PvSuffix("float32a")]
|
|
53
|
+
float64a: A[SignalRW[Array1D[np.float64]], PvSuffix("float64a")]
|
|
54
|
+
stra: A[SignalRW[Sequence[str]], PvSuffix("stra")]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ExamplePvaDevice(ExampleCaDevice): # pva can support all signal types that ca can
|
|
58
|
+
int8a: A[SignalRW[Array1D[np.int8]], PvSuffix("int8a")]
|
|
59
|
+
uint16a: A[SignalRW[Array1D[np.uint16]], PvSuffix("uint16a")]
|
|
60
|
+
uint32a: A[SignalRW[Array1D[np.uint32]], PvSuffix("uint32a")]
|
|
61
|
+
int64a: A[SignalRW[Array1D[np.int64]], PvSuffix("int64a")]
|
|
62
|
+
uint64a: A[SignalRW[Array1D[np.uint64]], PvSuffix("uint64a")]
|
|
63
|
+
table: A[SignalRW[ExampleTable], PvSuffix("table")]
|
|
64
|
+
ntndarray_data: A[SignalRW[Array1D[np.int64]], PvSuffix("ntndarray:data")]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def connect_example_device(
|
|
68
|
+
ioc: TestingIOC, protocol: Literal["ca", "pva"]
|
|
69
|
+
) -> ExamplePvaDevice | ExampleCaDevice:
|
|
70
|
+
"""Helper function to return a connected example device.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
|
|
75
|
+
ioc: TestingIOC
|
|
76
|
+
TestingIOC configured to provide the records needed for the device
|
|
77
|
+
|
|
78
|
+
protocol: Literal["ca", "pva"]
|
|
79
|
+
The transport protocol of the device
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
ExamplePvaDevice | ExampleCaDevice
|
|
84
|
+
a connected EpicsDevice with signals of many EPICS record types
|
|
85
|
+
"""
|
|
86
|
+
device_cls = ExamplePvaDevice if protocol == "pva" else ExampleCaDevice
|
|
87
|
+
device = device_cls(f"{protocol}://{ioc.prefix_for(device_cls)}")
|
|
88
|
+
await device.connect()
|
|
89
|
+
return device
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_example_ioc() -> TestingIOC:
|
|
93
|
+
"""Get TestingIOC instance with the example databases loaded.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
TestingIOC
|
|
98
|
+
instance with test_records.db loaded for ExampleCaDevice and
|
|
99
|
+
test_records.db and test_records_pva.db loaded for ExamplePvaDevice.
|
|
100
|
+
"""
|
|
101
|
+
ioc = TestingIOC()
|
|
102
|
+
ioc.database_for(PVA_RECORDS, ExamplePvaDevice)
|
|
103
|
+
ioc.database_for(CA_PVA_RECORDS, ExamplePvaDevice)
|
|
104
|
+
ioc.database_for(CA_PVA_RECORDS, ExampleCaDevice)
|
|
105
|
+
return ioc
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import string
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from aioca import purge_channel_caches
|
|
9
|
+
|
|
10
|
+
from ophyd_async.core import Device
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def generate_random_PV_prefix() -> str:
|
|
14
|
+
return "".join(random.choice(string.ascii_lowercase) for _ in range(12)) + ":"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestingIOC:
|
|
18
|
+
_dbs: dict[type[Device], list[Path]] = {}
|
|
19
|
+
_prefixes: dict[type[Device], str] = {}
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def with_database(cls, db: Path | str): # use as a decorator
|
|
23
|
+
def inner(device_cls: type[Device]):
|
|
24
|
+
cls.database_for(db, device_cls)
|
|
25
|
+
return device_cls
|
|
26
|
+
|
|
27
|
+
return inner
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def database_for(cls, db, device_cls):
|
|
31
|
+
path = Path(db)
|
|
32
|
+
if not path.is_file():
|
|
33
|
+
raise OSError(f"{path} is not a file.")
|
|
34
|
+
if device_cls not in cls._dbs:
|
|
35
|
+
cls._dbs[device_cls] = []
|
|
36
|
+
cls._dbs[device_cls].append(path)
|
|
37
|
+
|
|
38
|
+
def prefix_for(self, device_cls):
|
|
39
|
+
# generate random prefix, return existing if already generated
|
|
40
|
+
return self._prefixes.setdefault(device_cls, generate_random_PV_prefix())
|
|
41
|
+
|
|
42
|
+
def start_ioc(self):
|
|
43
|
+
ioc_args = [
|
|
44
|
+
sys.executable,
|
|
45
|
+
"-m",
|
|
46
|
+
"epicscorelibs.ioc",
|
|
47
|
+
]
|
|
48
|
+
for device_cls, dbs in self._dbs.items():
|
|
49
|
+
prefix = self.prefix_for(device_cls)
|
|
50
|
+
for db in dbs:
|
|
51
|
+
ioc_args += ["-m", f"device={prefix}", "-d", str(db)]
|
|
52
|
+
self._process = subprocess.Popen(
|
|
53
|
+
ioc_args,
|
|
54
|
+
stdin=subprocess.PIPE,
|
|
55
|
+
stdout=subprocess.PIPE,
|
|
56
|
+
stderr=subprocess.STDOUT,
|
|
57
|
+
universal_newlines=True,
|
|
58
|
+
)
|
|
59
|
+
start_time = time.monotonic()
|
|
60
|
+
while "iocRun: All initialization complete" not in (
|
|
61
|
+
self._process.stdout.readline().strip() # type: ignore
|
|
62
|
+
):
|
|
63
|
+
if time.monotonic() - start_time > 10:
|
|
64
|
+
try:
|
|
65
|
+
print(self._process.communicate("exit()")[0])
|
|
66
|
+
except ValueError:
|
|
67
|
+
# Someone else already called communicate
|
|
68
|
+
pass
|
|
69
|
+
raise TimeoutError("IOC did not start in time")
|
|
70
|
+
|
|
71
|
+
def stop_ioc(self):
|
|
72
|
+
# close backend caches before the event loop
|
|
73
|
+
purge_channel_caches()
|
|
74
|
+
try:
|
|
75
|
+
print(self._process.communicate("exit()")[0])
|
|
76
|
+
except ValueError:
|
|
77
|
+
# Someone else already called communicate
|
|
78
|
+
pass
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
record(bo, "$(device)bool") {
|
|
2
|
+
field(ZNAM, "No")
|
|
3
|
+
field(ONAM, "Yes")
|
|
4
|
+
field(VAL, "1")
|
|
5
|
+
field(PINI, "YES")
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
record(bo, "$(device)bool_unnamed") {
|
|
9
|
+
field(VAL, "1")
|
|
10
|
+
field(PINI, "YES")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
record(longout, "$(device)int") {
|
|
14
|
+
field(LLSV, "MAJOR") # LOLO is alarm
|
|
15
|
+
field(LSV, "MINOR") # LOW is warning
|
|
16
|
+
field(HSV, "MINOR") # HIGH is warning
|
|
17
|
+
field(HHSV, "MAJOR") # HIHI is alarm
|
|
18
|
+
field(HOPR, "100")
|
|
19
|
+
field(HIHI, "98")
|
|
20
|
+
field(HIGH, "96")
|
|
21
|
+
field(DRVH, "90")
|
|
22
|
+
field(DRVL, "10")
|
|
23
|
+
field(LOW, "5")
|
|
24
|
+
field(LOLO, "2")
|
|
25
|
+
field(LOPR, "0")
|
|
26
|
+
field(VAL, "42")
|
|
27
|
+
field(PINI, "YES")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
record(longout, "$(device)partialint") {
|
|
31
|
+
field(LLSV, "MAJOR") # LOLO is alarm
|
|
32
|
+
field(HHSV, "MAJOR") # HIHI is alarm
|
|
33
|
+
field(HOPR, "100")
|
|
34
|
+
field(HIHI, "98")
|
|
35
|
+
field(DRVH, "90")
|
|
36
|
+
field(DRVL, "10")
|
|
37
|
+
field(LOLO, "2")
|
|
38
|
+
field(LOPR, "0")
|
|
39
|
+
field(VAL, "42")
|
|
40
|
+
field(PINI, "YES")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
record(longout, "$(device)lessint") {
|
|
44
|
+
field(HSV, "MINOR") # LOW is warning
|
|
45
|
+
field(LSV, "MINOR") # HIGH is warning
|
|
46
|
+
field(HOPR, "100")
|
|
47
|
+
field(HIGH, "98")
|
|
48
|
+
field(LOW, "2")
|
|
49
|
+
field(LOPR, "0")
|
|
50
|
+
field(VAL, "42")
|
|
51
|
+
field(PINI, "YES")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
record(ao, "$(device)float") {
|
|
55
|
+
field(PREC, "1")
|
|
56
|
+
field(EGU, "mm")
|
|
57
|
+
field(VAL, "3.141")
|
|
58
|
+
field(PINI, "YES")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
record(ao, "$(device)float_prec_0") {
|
|
62
|
+
field(PREC, "0")
|
|
63
|
+
field(EGU, "mm")
|
|
64
|
+
field(VAL, "3")
|
|
65
|
+
field(PINI, "YES")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
record(ao, "$(device)float_prec_1") {
|
|
69
|
+
field(PREC, "1")
|
|
70
|
+
field(EGU, "mm")
|
|
71
|
+
field(VAL, "3")
|
|
72
|
+
field(PINI, "YES")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
record(stringout, "$(device)str") {
|
|
76
|
+
field(VAL, "hello")
|
|
77
|
+
field(PINI, "YES")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
record(mbbo, "$(device)enum") {
|
|
81
|
+
field(ZRST, "Aaa")
|
|
82
|
+
field(ZRVL, "5")
|
|
83
|
+
field(ONST, "Bbb")
|
|
84
|
+
field(ONVL, "6")
|
|
85
|
+
field(TWST, "Ccc")
|
|
86
|
+
field(TWVL, "7")
|
|
87
|
+
field(VAL, "1")
|
|
88
|
+
field(PINI, "YES")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
record(mbbo, "$(device)enum2") {
|
|
92
|
+
field(ZRST, "Aaa")
|
|
93
|
+
field(ONST, "Bbb")
|
|
94
|
+
field(TWST, "Ccc")
|
|
95
|
+
field(VAL, "1")
|
|
96
|
+
field(PINI, "YES")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
record(waveform, "$(device)uint8a") {
|
|
100
|
+
field(NELM, "3")
|
|
101
|
+
field(FTVL, "UCHAR")
|
|
102
|
+
field(INP, {const:[0, 255]})
|
|
103
|
+
field(PINI, "YES")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
record(waveform, "$(device)int16a") {
|
|
107
|
+
field(NELM, "3")
|
|
108
|
+
field(FTVL, "SHORT")
|
|
109
|
+
field(INP, {const:[-32768, 32767]})
|
|
110
|
+
field(PINI, "YES")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
record(waveform, "$(device)int32a") {
|
|
114
|
+
field(NELM, "3")
|
|
115
|
+
field(FTVL, "LONG")
|
|
116
|
+
field(INP, {const:[-2147483648, 2147483647]})
|
|
117
|
+
field(PINI, "YES")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
record(waveform, "$(device)float32a") {
|
|
121
|
+
field(NELM, "3")
|
|
122
|
+
field(FTVL, "FLOAT")
|
|
123
|
+
field(INP, {const:[0.000002, -123.123]})
|
|
124
|
+
field(PINI, "YES")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
record(waveform, "$(device)float64a") {
|
|
128
|
+
field(NELM, "3")
|
|
129
|
+
field(FTVL, "DOUBLE")
|
|
130
|
+
field(INP, {const:[0.1, -12345678.123]})
|
|
131
|
+
field(PINI, "YES")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
record(waveform, "$(device)stra") {
|
|
135
|
+
field(NELM, "3")
|
|
136
|
+
field(FTVL, "STRING")
|
|
137
|
+
field(INP, {const:["five", "six", "seven"]})
|
|
138
|
+
field(PINI, "YES")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
record(waveform, "$(device)longstr") {
|
|
142
|
+
field(NELM, "80")
|
|
143
|
+
field(FTVL, "CHAR")
|
|
144
|
+
field(INP, {const:"a string that is just longer than forty characters"})
|
|
145
|
+
field(PINI, "YES")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
record(lsi, "$(device)longstr2") {
|
|
149
|
+
field(SIZV, "80")
|
|
150
|
+
field(INP, {const:"a string that is just longer than forty characters"})
|
|
151
|
+
field(PINI, "YES")
|
|
152
|
+
}
|