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
|
@@ -3,13 +3,8 @@ from collections.abc import Sequence
|
|
|
3
3
|
|
|
4
4
|
from bluesky.protocols import Triggerable
|
|
5
5
|
|
|
6
|
-
from ophyd_async.core import
|
|
7
|
-
|
|
8
|
-
ConfigSignal,
|
|
9
|
-
HintedSignal,
|
|
10
|
-
SignalR,
|
|
11
|
-
StandardReadable,
|
|
12
|
-
)
|
|
6
|
+
from ophyd_async.core import AsyncStatus, SignalR, StandardReadable
|
|
7
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
13
8
|
|
|
14
9
|
from ._core_io import ADBaseIO, NDPluginBaseIO
|
|
15
10
|
from ._utils import ImageMode
|
|
@@ -24,14 +19,15 @@ class SingleTriggerDetector(StandardReadable, Triggerable):
|
|
|
24
19
|
**plugins: NDPluginBaseIO,
|
|
25
20
|
) -> None:
|
|
26
21
|
self.drv = drv
|
|
27
|
-
|
|
22
|
+
for k, v in plugins.items():
|
|
23
|
+
setattr(self, k, v)
|
|
28
24
|
|
|
29
25
|
self.add_readables(
|
|
30
26
|
[self.drv.array_counter, *read_uncached],
|
|
31
|
-
|
|
27
|
+
Format.HINTED_UNCACHED_SIGNAL,
|
|
32
28
|
)
|
|
33
29
|
|
|
34
|
-
self.add_readables([self.drv.acquire_time],
|
|
30
|
+
self.add_readables([self.drv.acquire_time], Format.CONFIG_SIGNAL)
|
|
35
31
|
|
|
36
32
|
super().__init__(name=name)
|
|
37
33
|
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from enum import Enum
|
|
3
2
|
|
|
4
|
-
from ophyd_async.core import
|
|
5
|
-
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
DEFAULT_TIMEOUT,
|
|
5
|
+
SignalDatatypeT,
|
|
6
|
+
SignalR,
|
|
7
|
+
SignalRW,
|
|
8
|
+
StrictEnum,
|
|
9
|
+
wait_for_value,
|
|
10
|
+
)
|
|
6
11
|
|
|
7
12
|
|
|
8
|
-
class ADBaseDataType(
|
|
13
|
+
class ADBaseDataType(StrictEnum):
|
|
9
14
|
Int8 = "Int8"
|
|
10
15
|
UInt8 = "UInt8"
|
|
11
16
|
Int16 = "Int16"
|
|
@@ -73,25 +78,25 @@ def convert_param_dtype_to_np(datatype: str) -> str:
|
|
|
73
78
|
return np_datatype
|
|
74
79
|
|
|
75
80
|
|
|
76
|
-
class FileWriteMode(
|
|
81
|
+
class FileWriteMode(StrictEnum):
|
|
77
82
|
single = "Single"
|
|
78
83
|
capture = "Capture"
|
|
79
84
|
stream = "Stream"
|
|
80
85
|
|
|
81
86
|
|
|
82
|
-
class ImageMode(
|
|
87
|
+
class ImageMode(StrictEnum):
|
|
83
88
|
single = "Single"
|
|
84
89
|
multiple = "Multiple"
|
|
85
90
|
continuous = "Continuous"
|
|
86
91
|
|
|
87
92
|
|
|
88
|
-
class NDAttributeDataType(
|
|
93
|
+
class NDAttributeDataType(StrictEnum):
|
|
89
94
|
INT = "INT"
|
|
90
95
|
DOUBLE = "DOUBLE"
|
|
91
96
|
STRING = "STRING"
|
|
92
97
|
|
|
93
98
|
|
|
94
|
-
class NDAttributePvDbrType(
|
|
99
|
+
class NDAttributePvDbrType(StrictEnum):
|
|
95
100
|
DBR_SHORT = "DBR_SHORT"
|
|
96
101
|
DBR_ENUM = "DBR_ENUM"
|
|
97
102
|
DBR_INT = "DBR_INT"
|
|
@@ -122,8 +127,8 @@ class NDAttributeParam:
|
|
|
122
127
|
|
|
123
128
|
|
|
124
129
|
async def stop_busy_record(
|
|
125
|
-
signal: SignalRW[
|
|
126
|
-
value:
|
|
130
|
+
signal: SignalRW[SignalDatatypeT],
|
|
131
|
+
value: SignalDatatypeT,
|
|
127
132
|
timeout: float = DEFAULT_TIMEOUT,
|
|
128
133
|
status_timeout: float | None = None,
|
|
129
134
|
) -> None:
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from ._kinetix import KinetixDetector
|
|
2
2
|
from ._kinetix_controller import KinetixController
|
|
3
|
-
from ._kinetix_io import KinetixDriverIO
|
|
3
|
+
from ._kinetix_io import KinetixDriverIO, KinetixTriggerMode
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
6
6
|
"KinetixDetector",
|
|
7
7
|
"KinetixController",
|
|
8
8
|
"KinetixDriverIO",
|
|
9
|
+
"KinetixTriggerMode",
|
|
9
10
|
]
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
|
-
from ophyd_async.core import
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
AsyncStatus,
|
|
5
|
+
DetectorController,
|
|
6
|
+
DetectorTrigger,
|
|
7
|
+
TriggerInfo,
|
|
8
|
+
)
|
|
6
9
|
from ophyd_async.epics import adcore
|
|
7
10
|
|
|
8
11
|
from ._kinetix_io import KinetixDriverIO, KinetixTriggerMode
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from ophyd_async.core import StrictEnum
|
|
3
2
|
from ophyd_async.epics import adcore
|
|
4
|
-
from ophyd_async.epics.
|
|
3
|
+
from ophyd_async.epics.core import epics_signal_rw_rbv
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
class KinetixTriggerMode(
|
|
6
|
+
class KinetixTriggerMode(StrictEnum):
|
|
8
7
|
internal = "Internal"
|
|
9
8
|
edge = "Rising Edge"
|
|
10
9
|
gate = "Exp. Gate"
|
|
11
10
|
|
|
12
11
|
|
|
13
|
-
class KinetixReadoutMode(
|
|
12
|
+
class KinetixReadoutMode(StrictEnum):
|
|
14
13
|
sensitivity = 1
|
|
15
14
|
speed = 2
|
|
16
15
|
dynamic_range = 3
|
|
@@ -2,12 +2,12 @@ import asyncio
|
|
|
2
2
|
|
|
3
3
|
from ophyd_async.core import (
|
|
4
4
|
DEFAULT_TIMEOUT,
|
|
5
|
+
AsyncStatus,
|
|
5
6
|
DetectorController,
|
|
6
7
|
DetectorTrigger,
|
|
8
|
+
TriggerInfo,
|
|
7
9
|
wait_for_value,
|
|
8
10
|
)
|
|
9
|
-
from ophyd_async.core._detector import TriggerInfo
|
|
10
|
-
from ophyd_async.core._status import AsyncStatus
|
|
11
11
|
from ophyd_async.epics import adcore
|
|
12
12
|
|
|
13
13
|
from ._pilatus_io import PilatusDriverIO, PilatusTriggerMode
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from ophyd_async.core import StrictEnum
|
|
3
2
|
from ophyd_async.epics import adcore
|
|
4
|
-
from ophyd_async.epics.
|
|
3
|
+
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw_rbv
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
class PilatusTriggerMode(
|
|
6
|
+
class PilatusTriggerMode(StrictEnum):
|
|
8
7
|
internal = "Internal"
|
|
9
8
|
ext_enable = "Ext. Enable"
|
|
10
9
|
ext_trigger = "Ext. Trigger"
|
|
@@ -2,11 +2,11 @@ import asyncio
|
|
|
2
2
|
|
|
3
3
|
from ophyd_async.core import (
|
|
4
4
|
DEFAULT_TIMEOUT,
|
|
5
|
+
AsyncStatus,
|
|
5
6
|
DetectorController,
|
|
6
7
|
DetectorTrigger,
|
|
8
|
+
TriggerInfo,
|
|
7
9
|
)
|
|
8
|
-
from ophyd_async.core._detector import TriggerInfo
|
|
9
|
-
from ophyd_async.core._status import AsyncStatus
|
|
10
10
|
from ophyd_async.epics import adcore
|
|
11
11
|
|
|
12
12
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from ._vimba import VimbaDetector
|
|
2
2
|
from ._vimba_controller import VimbaController
|
|
3
|
-
from ._vimba_io import VimbaDriverIO
|
|
3
|
+
from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
6
6
|
"VimbaDetector",
|
|
7
7
|
"VimbaController",
|
|
8
8
|
"VimbaDriverIO",
|
|
9
|
+
"VimbaExposeOutMode",
|
|
10
|
+
"VimbaOnOff",
|
|
11
|
+
"VimbaTriggerSource",
|
|
9
12
|
]
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
|
-
from ophyd_async.core import
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
from ophyd_async.core import (
|
|
4
|
+
AsyncStatus,
|
|
5
|
+
DetectorController,
|
|
6
|
+
DetectorTrigger,
|
|
7
|
+
TriggerInfo,
|
|
8
|
+
)
|
|
6
9
|
from ophyd_async.epics import adcore
|
|
7
10
|
|
|
8
11
|
from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from ophyd_async.core import StrictEnum
|
|
3
2
|
from ophyd_async.epics import adcore
|
|
4
|
-
from ophyd_async.epics.
|
|
3
|
+
from ophyd_async.epics.core import epics_signal_rw_rbv
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
class VimbaPixelFormat(
|
|
6
|
+
class VimbaPixelFormat(StrictEnum):
|
|
8
7
|
internal = "Mono8"
|
|
9
8
|
ext_enable = "Mono12"
|
|
10
9
|
ext_trigger = "Ext. Trigger"
|
|
@@ -12,7 +11,7 @@ class VimbaPixelFormat(str, Enum):
|
|
|
12
11
|
alignment = "Alignment"
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
class VimbaConvertFormat(
|
|
14
|
+
class VimbaConvertFormat(StrictEnum):
|
|
16
15
|
none = "None"
|
|
17
16
|
mono8 = "Mono8"
|
|
18
17
|
mono16 = "Mono16"
|
|
@@ -20,7 +19,7 @@ class VimbaConvertFormat(str, Enum):
|
|
|
20
19
|
rgb16 = "RGB16"
|
|
21
20
|
|
|
22
21
|
|
|
23
|
-
class VimbaTriggerSource(
|
|
22
|
+
class VimbaTriggerSource(StrictEnum):
|
|
24
23
|
freerun = "Freerun"
|
|
25
24
|
line1 = "Line1"
|
|
26
25
|
line2 = "Line2"
|
|
@@ -30,17 +29,17 @@ class VimbaTriggerSource(str, Enum):
|
|
|
30
29
|
action1 = "Action1"
|
|
31
30
|
|
|
32
31
|
|
|
33
|
-
class VimbaOverlap(
|
|
32
|
+
class VimbaOverlap(StrictEnum):
|
|
34
33
|
off = "Off"
|
|
35
34
|
prev_frame = "PreviousFrame"
|
|
36
35
|
|
|
37
36
|
|
|
38
|
-
class VimbaOnOff(
|
|
37
|
+
class VimbaOnOff(StrictEnum):
|
|
39
38
|
on = "On"
|
|
40
39
|
off = "Off"
|
|
41
40
|
|
|
42
41
|
|
|
43
|
-
class VimbaExposeOutMode(
|
|
42
|
+
class VimbaExposeOutMode(StrictEnum):
|
|
44
43
|
timed = "Timed" # Use ExposureTime PV
|
|
45
44
|
trigger_width = "TriggerWidth" # Expose for length of high signal
|
|
46
45
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from ._epics_connector import EpicsDeviceConnector, PvSuffix
|
|
2
|
+
from ._epics_device import EpicsDevice
|
|
3
|
+
from ._pvi_connector import PviDeviceConnector
|
|
4
|
+
from ._signal import (
|
|
5
|
+
CaSignalBackend,
|
|
6
|
+
PvaSignalBackend,
|
|
7
|
+
epics_signal_r,
|
|
8
|
+
epics_signal_rw,
|
|
9
|
+
epics_signal_rw_rbv,
|
|
10
|
+
epics_signal_w,
|
|
11
|
+
epics_signal_x,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"PviDeviceConnector",
|
|
16
|
+
"EpicsDeviceConnector",
|
|
17
|
+
"PvSuffix",
|
|
18
|
+
"EpicsDevice",
|
|
19
|
+
"CaSignalBackend",
|
|
20
|
+
"PvaSignalBackend",
|
|
21
|
+
"epics_signal_r",
|
|
22
|
+
"epics_signal_rw",
|
|
23
|
+
"epics_signal_rw_rbv",
|
|
24
|
+
"epics_signal_w",
|
|
25
|
+
"epics_signal_x",
|
|
26
|
+
]
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from math import isnan, nan
|
|
5
|
+
from typing import Any, Generic, cast
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from aioca import (
|
|
9
|
+
FORMAT_CTRL,
|
|
10
|
+
FORMAT_RAW,
|
|
11
|
+
FORMAT_TIME,
|
|
12
|
+
CANothing,
|
|
13
|
+
Subscription,
|
|
14
|
+
caget,
|
|
15
|
+
camonitor,
|
|
16
|
+
caput,
|
|
17
|
+
)
|
|
18
|
+
from aioca.types import AugmentedValue, Dbr, Format
|
|
19
|
+
from bluesky.protocols import Reading
|
|
20
|
+
from epicscorelibs.ca import dbr
|
|
21
|
+
from event_model import DataKey, Limits, LimitsRange
|
|
22
|
+
|
|
23
|
+
from ophyd_async.core import (
|
|
24
|
+
Array1D,
|
|
25
|
+
Callback,
|
|
26
|
+
NotConnected,
|
|
27
|
+
SignalDatatype,
|
|
28
|
+
SignalDatatypeT,
|
|
29
|
+
SignalMetadata,
|
|
30
|
+
get_enum_cls,
|
|
31
|
+
get_unique,
|
|
32
|
+
make_datakey,
|
|
33
|
+
wait_for_connection,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from ._util import EpicsSignalBackend, format_datatype, get_supported_values
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
|
|
40
|
+
def get_limits(limit: str) -> LimitsRange | None:
|
|
41
|
+
low = getattr(value, f"lower_{limit}_limit", nan)
|
|
42
|
+
high = getattr(value, f"upper_{limit}_limit", nan)
|
|
43
|
+
if not (isnan(low) and isnan(high)):
|
|
44
|
+
return LimitsRange(
|
|
45
|
+
low=None if isnan(low) else low,
|
|
46
|
+
high=None if isnan(high) else high,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
limits = Limits()
|
|
50
|
+
if limits_range := get_limits("alarm"):
|
|
51
|
+
limits["alarm"] = limits_range
|
|
52
|
+
if limits_range := get_limits("ctrl"):
|
|
53
|
+
limits["control"] = limits_range
|
|
54
|
+
if limits_range := get_limits("disp"):
|
|
55
|
+
limits["display"] = limits_range
|
|
56
|
+
if limits_range := get_limits("warning"):
|
|
57
|
+
limits["warning"] = limits_range
|
|
58
|
+
return limits
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _metadata_from_augmented_value(
|
|
62
|
+
value: AugmentedValue, metadata: SignalMetadata
|
|
63
|
+
) -> SignalMetadata:
|
|
64
|
+
metadata = metadata.copy()
|
|
65
|
+
if hasattr(value, "units"):
|
|
66
|
+
metadata["units"] = value.units
|
|
67
|
+
if hasattr(value, "precision") and not isnan(value.precision):
|
|
68
|
+
metadata["precision"] = value.precision
|
|
69
|
+
if limits := _limits_from_augmented_value(value):
|
|
70
|
+
metadata["limits"] = limits
|
|
71
|
+
return metadata
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CaConverter(Generic[SignalDatatypeT]):
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
datatype: type[SignalDatatypeT],
|
|
78
|
+
read_dbr: Dbr,
|
|
79
|
+
write_dbr: Dbr | None = None,
|
|
80
|
+
metadata: SignalMetadata | None = None,
|
|
81
|
+
):
|
|
82
|
+
self.datatype = datatype
|
|
83
|
+
self.read_dbr: Dbr = read_dbr
|
|
84
|
+
self.write_dbr: Dbr | None = write_dbr
|
|
85
|
+
self.metadata = metadata or SignalMetadata()
|
|
86
|
+
|
|
87
|
+
def write_value(self, value: Any) -> Any:
|
|
88
|
+
# The ca library will do the conversion for us
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
def value(self, value: AugmentedValue) -> SignalDatatypeT:
|
|
92
|
+
# for channel access ca_xxx classes, this
|
|
93
|
+
# invokes __pos__ operator to return an instance of
|
|
94
|
+
# the builtin base class
|
|
95
|
+
return +value # type: ignore
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class DisconnectedCaConverter(CaConverter):
|
|
99
|
+
def __getattribute__(self, __name: str) -> Any:
|
|
100
|
+
raise NotImplementedError("No PV has been set as connect() has not been called")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class CaArrayConverter(CaConverter[np.ndarray]):
|
|
104
|
+
def value(self, value: AugmentedValue) -> np.ndarray:
|
|
105
|
+
# A less expensive conversion
|
|
106
|
+
return np.array(value, copy=False)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class CaSequenceStrConverter(CaConverter[Sequence[str]]):
|
|
110
|
+
def value(self, value: AugmentedValue) -> Sequence[str]:
|
|
111
|
+
return [str(v) for v in value] # type: ignore
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class CaLongStrConverter(CaConverter[str]):
|
|
115
|
+
def __init__(self):
|
|
116
|
+
super().__init__(str, dbr.DBR_CHAR_STR, dbr.DBR_CHAR_STR)
|
|
117
|
+
|
|
118
|
+
def write_value(self, value: Any) -> Any:
|
|
119
|
+
# Add a null in here as this is what the commandline caput does
|
|
120
|
+
# TODO: this should be in the server so check if it can be pushed to asyn
|
|
121
|
+
return value + "\0"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class CaBoolConverter(CaConverter[bool]):
|
|
125
|
+
def __init__(self):
|
|
126
|
+
super().__init__(bool, dbr.DBR_SHORT)
|
|
127
|
+
|
|
128
|
+
def value(self, value: AugmentedValue) -> bool:
|
|
129
|
+
return bool(value)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class CaEnumConverter(CaConverter[str]):
|
|
133
|
+
def __init__(self, supported_values: dict[str, str]):
|
|
134
|
+
self.supported_values = supported_values
|
|
135
|
+
super().__init__(
|
|
136
|
+
str, dbr.DBR_STRING, metadata=SignalMetadata(choices=list(supported_values))
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def value(self, value: AugmentedValue) -> str:
|
|
140
|
+
return self.supported_values[str(value)]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
_datatype_converter_from_dbr: dict[
|
|
144
|
+
tuple[Dbr, bool], tuple[type[SignalDatatype], type[CaConverter]]
|
|
145
|
+
] = {
|
|
146
|
+
(dbr.DBR_STRING, False): (str, CaConverter),
|
|
147
|
+
(dbr.DBR_SHORT, False): (int, CaConverter),
|
|
148
|
+
(dbr.DBR_FLOAT, False): (float, CaConverter),
|
|
149
|
+
(dbr.DBR_ENUM, False): (str, CaConverter),
|
|
150
|
+
(dbr.DBR_CHAR, False): (int, CaConverter),
|
|
151
|
+
(dbr.DBR_LONG, False): (int, CaConverter),
|
|
152
|
+
(dbr.DBR_DOUBLE, False): (float, CaConverter),
|
|
153
|
+
(dbr.DBR_STRING, True): (Sequence[str], CaSequenceStrConverter),
|
|
154
|
+
(dbr.DBR_SHORT, True): (Array1D[np.int16], CaArrayConverter),
|
|
155
|
+
(dbr.DBR_FLOAT, True): (Array1D[np.float32], CaArrayConverter),
|
|
156
|
+
(dbr.DBR_ENUM, True): (Sequence[str], CaSequenceStrConverter),
|
|
157
|
+
(dbr.DBR_CHAR, True): (Array1D[np.uint8], CaArrayConverter),
|
|
158
|
+
(dbr.DBR_LONG, True): (Array1D[np.int32], CaArrayConverter),
|
|
159
|
+
(dbr.DBR_DOUBLE, True): (Array1D[np.float64], CaArrayConverter),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def make_converter(
|
|
164
|
+
datatype: type | None, values: dict[str, AugmentedValue]
|
|
165
|
+
) -> CaConverter:
|
|
166
|
+
pv = list(values)[0]
|
|
167
|
+
pv_dbr = cast(
|
|
168
|
+
Dbr, get_unique({k: v.datatype for k, v in values.items()}, "datatypes")
|
|
169
|
+
)
|
|
170
|
+
is_array = bool([v for v in values.values() if v.element_count > 1])
|
|
171
|
+
# Infer a datatype and converter from the dbr
|
|
172
|
+
inferred_datatype, converter_cls = _datatype_converter_from_dbr[(pv_dbr, is_array)]
|
|
173
|
+
# Some override cases
|
|
174
|
+
if is_array and pv_dbr == dbr.DBR_CHAR and datatype is str:
|
|
175
|
+
# Override waveform of chars to be treated as string
|
|
176
|
+
return CaLongStrConverter()
|
|
177
|
+
elif not is_array and datatype is bool and pv_dbr == dbr.DBR_ENUM:
|
|
178
|
+
# Database can't do bools, so are often representated as enums of len 2
|
|
179
|
+
pv_num_choices = get_unique(
|
|
180
|
+
{k: len(v.enums) for k, v in values.items()}, "number of choices"
|
|
181
|
+
)
|
|
182
|
+
if pv_num_choices != 2:
|
|
183
|
+
raise TypeError(f"{pv} has {pv_num_choices} choices, can't map to bool")
|
|
184
|
+
return CaBoolConverter()
|
|
185
|
+
elif not is_array and pv_dbr == dbr.DBR_ENUM:
|
|
186
|
+
pv_choices = get_unique(
|
|
187
|
+
{k: tuple(v.enums) for k, v in values.items()}, "choices"
|
|
188
|
+
)
|
|
189
|
+
if enum_cls := get_enum_cls(datatype):
|
|
190
|
+
# If explicitly requested then check
|
|
191
|
+
return CaEnumConverter(get_supported_values(pv, enum_cls, pv_choices))
|
|
192
|
+
elif datatype in (None, str):
|
|
193
|
+
# Drop to string for safety, but retain choices as metadata
|
|
194
|
+
return CaConverter(
|
|
195
|
+
str,
|
|
196
|
+
dbr.DBR_STRING,
|
|
197
|
+
metadata=SignalMetadata(choices=list(pv_choices)),
|
|
198
|
+
)
|
|
199
|
+
elif (
|
|
200
|
+
inferred_datatype is float
|
|
201
|
+
and datatype is int
|
|
202
|
+
and get_unique({k: v.precision for k, v in values.items()}, "precision") == 0
|
|
203
|
+
):
|
|
204
|
+
# Allow int signals to represent float records when prec is 0
|
|
205
|
+
return CaConverter(int, pv_dbr)
|
|
206
|
+
elif datatype in (None, inferred_datatype):
|
|
207
|
+
# If datatype matches what we are given then allow it and use inferred converter
|
|
208
|
+
return converter_cls(inferred_datatype, pv_dbr)
|
|
209
|
+
if pv_dbr == dbr.DBR_ENUM:
|
|
210
|
+
inferred_datatype = "str | SubsetEnum | StrictEnum"
|
|
211
|
+
raise TypeError(
|
|
212
|
+
f"{pv} with inferred datatype {format_datatype(inferred_datatype)}"
|
|
213
|
+
f" cannot be coerced to {format_datatype(datatype)}"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
_tried_pyepics = False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _use_pyepics_context_if_imported():
|
|
221
|
+
global _tried_pyepics
|
|
222
|
+
if not _tried_pyepics:
|
|
223
|
+
ca = sys.modules.get("epics.ca", None)
|
|
224
|
+
if ca:
|
|
225
|
+
ca.use_initial_context()
|
|
226
|
+
_tried_pyepics = True
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
|
|
230
|
+
def __init__(
|
|
231
|
+
self,
|
|
232
|
+
datatype: type[SignalDatatypeT] | None,
|
|
233
|
+
read_pv: str = "",
|
|
234
|
+
write_pv: str = "",
|
|
235
|
+
):
|
|
236
|
+
self.converter: CaConverter = DisconnectedCaConverter(float, dbr.DBR_DOUBLE)
|
|
237
|
+
self.initial_values: dict[str, AugmentedValue] = {}
|
|
238
|
+
self.subscription: Subscription | None = None
|
|
239
|
+
super().__init__(datatype, read_pv, write_pv)
|
|
240
|
+
|
|
241
|
+
def source(self, name: str, read: bool):
|
|
242
|
+
return f"ca://{self.read_pv if read else self.write_pv}"
|
|
243
|
+
|
|
244
|
+
async def _store_initial_value(self, pv: str, timeout: float):
|
|
245
|
+
try:
|
|
246
|
+
self.initial_values[pv] = await caget(
|
|
247
|
+
pv, format=FORMAT_CTRL, timeout=timeout
|
|
248
|
+
)
|
|
249
|
+
except CANothing as exc:
|
|
250
|
+
logging.debug(f"signal ca://{pv} timed out")
|
|
251
|
+
raise NotConnected(f"ca://{pv}") from exc
|
|
252
|
+
|
|
253
|
+
async def connect(self, timeout: float):
|
|
254
|
+
_use_pyepics_context_if_imported()
|
|
255
|
+
if self.read_pv != self.write_pv:
|
|
256
|
+
# Different, need to connect both
|
|
257
|
+
await wait_for_connection(
|
|
258
|
+
read_pv=self._store_initial_value(self.read_pv, timeout=timeout),
|
|
259
|
+
write_pv=self._store_initial_value(self.write_pv, timeout=timeout),
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
# The same, so only need to connect one
|
|
263
|
+
await self._store_initial_value(self.read_pv, timeout=timeout)
|
|
264
|
+
self.converter = make_converter(self.datatype, self.initial_values)
|
|
265
|
+
|
|
266
|
+
async def _caget(self, pv: str, format: Format) -> AugmentedValue:
|
|
267
|
+
return await caget(
|
|
268
|
+
pv, datatype=self.converter.read_dbr, format=format, timeout=None
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def _make_reading(self, value: AugmentedValue) -> Reading[SignalDatatypeT]:
|
|
272
|
+
return {
|
|
273
|
+
"value": self.converter.value(value),
|
|
274
|
+
"timestamp": value.timestamp,
|
|
275
|
+
"alarm_severity": -1 if value.severity > 2 else value.severity,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async def put(self, value: SignalDatatypeT | None, wait: bool):
|
|
279
|
+
if value is None:
|
|
280
|
+
write_value = self.initial_values[self.write_pv]
|
|
281
|
+
else:
|
|
282
|
+
write_value = self.converter.write_value(value)
|
|
283
|
+
await caput(
|
|
284
|
+
self.write_pv,
|
|
285
|
+
write_value,
|
|
286
|
+
datatype=self.converter.write_dbr,
|
|
287
|
+
wait=wait,
|
|
288
|
+
timeout=None,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
async def get_datakey(self, source: str) -> DataKey:
|
|
292
|
+
value = await self._caget(self.read_pv, FORMAT_CTRL)
|
|
293
|
+
metadata = _metadata_from_augmented_value(value, self.converter.metadata)
|
|
294
|
+
return make_datakey(
|
|
295
|
+
self.converter.datatype, self.converter.value(value), source, metadata
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
async def get_reading(self) -> Reading[SignalDatatypeT]:
|
|
299
|
+
value = await self._caget(self.read_pv, FORMAT_TIME)
|
|
300
|
+
return self._make_reading(value)
|
|
301
|
+
|
|
302
|
+
async def get_value(self) -> SignalDatatypeT:
|
|
303
|
+
value = await self._caget(self.read_pv, FORMAT_RAW)
|
|
304
|
+
return self.converter.value(value)
|
|
305
|
+
|
|
306
|
+
async def get_setpoint(self) -> SignalDatatypeT:
|
|
307
|
+
value = await self._caget(self.write_pv, FORMAT_RAW)
|
|
308
|
+
return self.converter.value(value)
|
|
309
|
+
|
|
310
|
+
def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
|
|
311
|
+
if callback:
|
|
312
|
+
assert (
|
|
313
|
+
not self.subscription
|
|
314
|
+
), "Cannot set a callback when one is already set"
|
|
315
|
+
self.subscription = camonitor(
|
|
316
|
+
self.read_pv,
|
|
317
|
+
lambda v: callback(self._make_reading(v)),
|
|
318
|
+
datatype=self.converter.read_dbr,
|
|
319
|
+
format=FORMAT_TIME,
|
|
320
|
+
)
|
|
321
|
+
elif self.subscription:
|
|
322
|
+
self.subscription.close()
|
|
323
|
+
self.subscription = None
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ophyd_async.core import Device, DeviceConnector, DeviceFiller
|
|
7
|
+
|
|
8
|
+
from ._signal import EpicsSignalBackend, get_signal_backend_type, split_protocol_from_pv
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class PvSuffix:
|
|
13
|
+
read_suffix: str
|
|
14
|
+
write_suffix: str | None = None
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def rbv(cls, write_suffix: str, rbv_suffix: str = "_RBV") -> PvSuffix:
|
|
18
|
+
return cls(write_suffix + rbv_suffix, write_suffix)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def fill_backend_with_prefix(
|
|
22
|
+
prefix: str, backend: EpicsSignalBackend, annotations: list[Any]
|
|
23
|
+
):
|
|
24
|
+
unhandled = []
|
|
25
|
+
while annotations:
|
|
26
|
+
annotation = annotations.pop(0)
|
|
27
|
+
if isinstance(annotation, PvSuffix):
|
|
28
|
+
backend.read_pv = prefix + annotation.read_suffix
|
|
29
|
+
backend.write_pv = prefix + (
|
|
30
|
+
annotation.write_suffix or annotation.read_suffix
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
unhandled.append(annotation)
|
|
34
|
+
annotations.extend(unhandled)
|
|
35
|
+
# These leftover annotations will now be handled by the iterator
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class EpicsDeviceConnector(DeviceConnector):
|
|
39
|
+
def __init__(self, prefix: str) -> None:
|
|
40
|
+
self.prefix = prefix
|
|
41
|
+
|
|
42
|
+
def create_children_from_annotations(self, device: Device):
|
|
43
|
+
if not hasattr(self, "filler"):
|
|
44
|
+
protocol, prefix = split_protocol_from_pv(self.prefix)
|
|
45
|
+
self.filler = DeviceFiller(
|
|
46
|
+
device,
|
|
47
|
+
signal_backend_factory=get_signal_backend_type(protocol),
|
|
48
|
+
device_connector_factory=DeviceConnector,
|
|
49
|
+
)
|
|
50
|
+
for backend, annotations in self.filler.create_signals_from_annotations():
|
|
51
|
+
fill_backend_with_prefix(prefix, backend, annotations)
|
|
52
|
+
|
|
53
|
+
list(self.filler.create_devices_from_annotations())
|