ophyd-async 0.5.2__py3-none-any.whl → 0.6.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/__init__.py +10 -1
- ophyd_async/__main__.py +12 -4
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +11 -3
- ophyd_async/core/_detector.py +72 -63
- ophyd_async/core/_device.py +13 -15
- ophyd_async/core/_device_save_loader.py +30 -19
- ophyd_async/core/_flyer.py +6 -4
- ophyd_async/core/_hdf_dataset.py +8 -9
- ophyd_async/core/_log.py +3 -1
- ophyd_async/core/_mock_signal_backend.py +11 -9
- ophyd_async/core/_mock_signal_utils.py +8 -5
- ophyd_async/core/_protocol.py +7 -7
- ophyd_async/core/_providers.py +11 -11
- ophyd_async/core/_readable.py +30 -22
- ophyd_async/core/_signal.py +52 -51
- ophyd_async/core/_signal_backend.py +20 -7
- ophyd_async/core/_soft_signal_backend.py +62 -32
- ophyd_async/core/_status.py +7 -9
- ophyd_async/core/_table.py +63 -0
- ophyd_async/core/_utils.py +24 -28
- ophyd_async/epics/adaravis/_aravis_controller.py +17 -16
- ophyd_async/epics/adaravis/_aravis_io.py +2 -1
- ophyd_async/epics/adcore/_core_io.py +2 -0
- ophyd_async/epics/adcore/_core_logic.py +2 -3
- ophyd_async/epics/adcore/_hdf_writer.py +19 -8
- ophyd_async/epics/adcore/_single_trigger.py +1 -1
- ophyd_async/epics/adcore/_utils.py +5 -6
- ophyd_async/epics/adkinetix/_kinetix_controller.py +19 -14
- ophyd_async/epics/adpilatus/_pilatus_controller.py +18 -16
- ophyd_async/epics/adsimdetector/_sim.py +6 -5
- ophyd_async/epics/adsimdetector/_sim_controller.py +20 -15
- ophyd_async/epics/advimba/_vimba_controller.py +21 -16
- ophyd_async/epics/demo/_mover.py +4 -5
- ophyd_async/epics/demo/sensor.db +0 -1
- ophyd_async/epics/eiger/_eiger.py +1 -1
- ophyd_async/epics/eiger/_eiger_controller.py +16 -16
- ophyd_async/epics/eiger/_odin_io.py +6 -5
- ophyd_async/epics/motor.py +8 -10
- ophyd_async/epics/pvi/_pvi.py +30 -33
- ophyd_async/epics/signal/_aioca.py +55 -25
- ophyd_async/epics/signal/_common.py +3 -10
- ophyd_async/epics/signal/_epics_transport.py +11 -8
- ophyd_async/epics/signal/_p4p.py +79 -30
- ophyd_async/epics/signal/_signal.py +6 -8
- ophyd_async/fastcs/panda/__init__.py +0 -6
- ophyd_async/fastcs/panda/_control.py +14 -15
- ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
- ophyd_async/fastcs/panda/_table.py +111 -138
- ophyd_async/fastcs/panda/_trigger.py +1 -2
- ophyd_async/fastcs/panda/_utils.py +3 -2
- ophyd_async/fastcs/panda/_writer.py +28 -13
- ophyd_async/plan_stubs/_fly.py +16 -16
- ophyd_async/plan_stubs/_nd_attributes.py +12 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +24 -20
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
- ophyd_async/sim/demo/_sim_motor.py +2 -1
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/METADATA +46 -45
- ophyd_async-0.6.0.dist-info/RECORD +96 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/WHEEL +1 -1
- ophyd_async-0.5.2.dist-info/RECORD +0 -95
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/top_level.txt +0 -0
|
@@ -38,6 +38,6 @@ class EigerDetector(StandardDetector):
|
|
|
38
38
|
)
|
|
39
39
|
|
|
40
40
|
@AsyncStatus.wrap
|
|
41
|
-
async def prepare(self, value: EigerTriggerInfo) -> None:
|
|
41
|
+
async def prepare(self, value: EigerTriggerInfo) -> None: # type: ignore
|
|
42
42
|
await self._controller.set_energy(value.energy_ev)
|
|
43
43
|
await super().prepare(value)
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from typing import Optional
|
|
3
2
|
|
|
4
3
|
from ophyd_async.core import (
|
|
5
4
|
DEFAULT_TIMEOUT,
|
|
6
|
-
AsyncStatus,
|
|
7
5
|
DetectorControl,
|
|
8
6
|
DetectorTrigger,
|
|
9
7
|
set_and_wait_for_other_value,
|
|
10
8
|
)
|
|
9
|
+
from ophyd_async.core._detector import TriggerInfo
|
|
11
10
|
|
|
12
11
|
from ._eiger_io import EigerDriverIO, EigerTriggerMode
|
|
13
12
|
|
|
@@ -26,7 +25,7 @@ class EigerController(DetectorControl):
|
|
|
26
25
|
) -> None:
|
|
27
26
|
self._drv = driver
|
|
28
27
|
|
|
29
|
-
def get_deadtime(self, exposure: float) -> float:
|
|
28
|
+
def get_deadtime(self, exposure: float | None) -> float:
|
|
30
29
|
# See https://media.dectris.com/filer_public/30/14/3014704e-5f3b-43ba-8ccf-8ef720e60d2a/240202_usermanual_eiger2.pdf
|
|
31
30
|
return 0.0001
|
|
32
31
|
|
|
@@ -37,30 +36,31 @@ class EigerController(DetectorControl):
|
|
|
37
36
|
if abs(current_energy - energy) > tolerance:
|
|
38
37
|
await self._drv.photon_energy.set(energy)
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
async def arm(
|
|
42
|
-
self,
|
|
43
|
-
num: int,
|
|
44
|
-
trigger: DetectorTrigger = DetectorTrigger.internal,
|
|
45
|
-
exposure: Optional[float] = None,
|
|
46
|
-
):
|
|
39
|
+
async def prepare(self, trigger_info: TriggerInfo):
|
|
47
40
|
coros = [
|
|
48
|
-
self._drv.trigger_mode.set(
|
|
49
|
-
|
|
41
|
+
self._drv.trigger_mode.set(
|
|
42
|
+
EIGER_TRIGGER_MODE_MAP[trigger_info.trigger].value
|
|
43
|
+
),
|
|
44
|
+
self._drv.num_images.set(trigger_info.number),
|
|
50
45
|
]
|
|
51
|
-
if
|
|
46
|
+
if trigger_info.livetime is not None:
|
|
52
47
|
coros.extend(
|
|
53
48
|
[
|
|
54
|
-
self._drv.acquire_time.set(
|
|
55
|
-
self._drv.acquire_period.set(
|
|
49
|
+
self._drv.acquire_time.set(trigger_info.livetime),
|
|
50
|
+
self._drv.acquire_period.set(trigger_info.livetime),
|
|
56
51
|
]
|
|
57
52
|
)
|
|
58
53
|
await asyncio.gather(*coros)
|
|
59
54
|
|
|
55
|
+
async def arm(self):
|
|
60
56
|
# TODO: Detector state should be an enum see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
|
|
61
|
-
|
|
57
|
+
self._arm_status = set_and_wait_for_other_value(
|
|
62
58
|
self._drv.arm, 1, self._drv.state, "ready", timeout=DEFAULT_TIMEOUT
|
|
63
59
|
)
|
|
64
60
|
|
|
61
|
+
async def wait_for_idle(self):
|
|
62
|
+
if self._arm_status:
|
|
63
|
+
await self._arm_status
|
|
64
|
+
|
|
65
65
|
async def disarm(self):
|
|
66
66
|
await self._drv.disarm.set(1)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
|
2
3
|
from enum import Enum
|
|
3
|
-
from typing import AsyncGenerator, AsyncIterator, Dict
|
|
4
4
|
|
|
5
5
|
from bluesky.protocols import StreamAsset
|
|
6
|
-
from event_model
|
|
6
|
+
from event_model import DataKey
|
|
7
7
|
|
|
8
8
|
from ophyd_async.core import (
|
|
9
9
|
DEFAULT_TIMEOUT,
|
|
@@ -77,7 +77,7 @@ class OdinWriter(DetectorWriter):
|
|
|
77
77
|
self._name_provider = name_provider
|
|
78
78
|
super().__init__()
|
|
79
79
|
|
|
80
|
-
async def open(self, multiplier: int = 1) ->
|
|
80
|
+
async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
|
|
81
81
|
info = self._path_provider(device_name=self._name_provider())
|
|
82
82
|
|
|
83
83
|
await asyncio.gather(
|
|
@@ -93,7 +93,7 @@ class OdinWriter(DetectorWriter):
|
|
|
93
93
|
|
|
94
94
|
return await self._describe()
|
|
95
95
|
|
|
96
|
-
async def _describe(self) ->
|
|
96
|
+
async def _describe(self) -> dict[str, DataKey]:
|
|
97
97
|
data_shape = await asyncio.gather(
|
|
98
98
|
self._drv.image_height.get_value(), self._drv.image_width.get_value()
|
|
99
99
|
)
|
|
@@ -103,7 +103,8 @@ class OdinWriter(DetectorWriter):
|
|
|
103
103
|
source=self._drv.file_name.source,
|
|
104
104
|
shape=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", # type: ignore
|
|
107
108
|
external="STREAM:",
|
|
108
109
|
)
|
|
109
110
|
}
|
ophyd_async/epics/motor.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from typing import Optional
|
|
3
2
|
|
|
4
3
|
from bluesky.protocols import (
|
|
5
4
|
Flyable,
|
|
@@ -11,10 +10,10 @@ from bluesky.protocols import (
|
|
|
11
10
|
from pydantic import BaseModel, Field
|
|
12
11
|
|
|
13
12
|
from ophyd_async.core import (
|
|
13
|
+
CALCULATE_TIMEOUT,
|
|
14
14
|
DEFAULT_TIMEOUT,
|
|
15
15
|
AsyncStatus,
|
|
16
16
|
CalculatableTimeout,
|
|
17
|
-
CalculateTimeout,
|
|
18
17
|
ConfigSignal,
|
|
19
18
|
HintedSignal,
|
|
20
19
|
StandardReadable,
|
|
@@ -54,7 +53,7 @@ class FlyMotorInfo(BaseModel):
|
|
|
54
53
|
|
|
55
54
|
#: Maximum time for the complete motor move, including run up and run down.
|
|
56
55
|
#: Defaults to `time_for_move` + run up and run down times + 10s.
|
|
57
|
-
timeout: CalculatableTimeout = Field(frozen=True, default=
|
|
56
|
+
timeout: CalculatableTimeout = Field(frozen=True, default=CALCULATE_TIMEOUT)
|
|
58
57
|
|
|
59
58
|
|
|
60
59
|
class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
@@ -83,13 +82,13 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
83
82
|
self._set_success = True
|
|
84
83
|
|
|
85
84
|
# end_position of a fly move, with run_up_distance added on.
|
|
86
|
-
self._fly_completed_position:
|
|
85
|
+
self._fly_completed_position: float | None = None
|
|
87
86
|
|
|
88
87
|
# Set on kickoff(), complete when motor reaches self._fly_completed_position
|
|
89
|
-
self._fly_status:
|
|
88
|
+
self._fly_status: WatchableAsyncStatus | None = None
|
|
90
89
|
|
|
91
90
|
# Set during prepare
|
|
92
|
-
self._fly_timeout:
|
|
91
|
+
self._fly_timeout: CalculatableTimeout | None = CALCULATE_TIMEOUT
|
|
93
92
|
|
|
94
93
|
super().__init__(name=name)
|
|
95
94
|
|
|
@@ -138,9 +137,8 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
138
137
|
return self._fly_status
|
|
139
138
|
|
|
140
139
|
@WatchableAsyncStatus.wrap
|
|
141
|
-
async def set(
|
|
142
|
-
|
|
143
|
-
):
|
|
140
|
+
async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT):
|
|
141
|
+
new_position = value
|
|
144
142
|
self._set_success = True
|
|
145
143
|
(
|
|
146
144
|
old_position,
|
|
@@ -155,7 +153,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
155
153
|
self.velocity.get_value(),
|
|
156
154
|
self.acceleration_time.get_value(),
|
|
157
155
|
)
|
|
158
|
-
if timeout is
|
|
156
|
+
if timeout is CALCULATE_TIMEOUT:
|
|
159
157
|
assert velocity > 0, "Motor has zero velocity"
|
|
160
158
|
timeout = (
|
|
161
159
|
abs(new_position - old_position) / velocity
|
ophyd_async/epics/pvi/_pvi.py
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import re
|
|
2
|
+
import types
|
|
3
|
+
from collections.abc import Callable
|
|
2
4
|
from dataclasses import dataclass
|
|
3
5
|
from inspect import isclass
|
|
4
6
|
from typing import (
|
|
5
7
|
Any,
|
|
6
|
-
Callable,
|
|
7
|
-
Dict,
|
|
8
|
-
FrozenSet,
|
|
9
8
|
Literal,
|
|
10
|
-
Optional,
|
|
11
|
-
Tuple,
|
|
12
|
-
Type,
|
|
13
9
|
Union,
|
|
14
10
|
get_args,
|
|
15
11
|
get_origin,
|
|
@@ -32,23 +28,24 @@ from ophyd_async.epics.signal import (
|
|
|
32
28
|
epics_signal_x,
|
|
33
29
|
)
|
|
34
30
|
|
|
35
|
-
Access =
|
|
36
|
-
|
|
31
|
+
Access = frozenset[
|
|
32
|
+
Literal["r"] | Literal["w"] | Literal["rw"] | Literal["x"] | Literal["d"]
|
|
37
33
|
]
|
|
38
34
|
|
|
39
35
|
|
|
40
|
-
def _strip_number_from_string(string: str) ->
|
|
36
|
+
def _strip_number_from_string(string: str) -> tuple[str, int | None]:
|
|
41
37
|
match = re.match(r"(.*?)(\d*)$", string)
|
|
42
38
|
assert match
|
|
43
39
|
|
|
44
40
|
name = match.group(1)
|
|
45
41
|
number = match.group(2) or None
|
|
46
|
-
if number:
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
if number is None:
|
|
43
|
+
return name, None
|
|
44
|
+
else:
|
|
45
|
+
return name, int(number)
|
|
49
46
|
|
|
50
47
|
|
|
51
|
-
def _split_subscript(tp: T) ->
|
|
48
|
+
def _split_subscript(tp: T) -> tuple[Any, tuple[Any]] | tuple[T, None]:
|
|
52
49
|
"""Split a subscripted type into the its origin and args.
|
|
53
50
|
|
|
54
51
|
If `tp` is not a subscripted type, then just return the type and None as args.
|
|
@@ -60,8 +57,8 @@ def _split_subscript(tp: T) -> Union[Tuple[Any, Tuple[Any]], Tuple[T, None]]:
|
|
|
60
57
|
return tp, None
|
|
61
58
|
|
|
62
59
|
|
|
63
|
-
def _strip_union(field:
|
|
64
|
-
if get_origin(field)
|
|
60
|
+
def _strip_union(field: T | T) -> tuple[T, bool]:
|
|
61
|
+
if get_origin(field) in [Union, types.UnionType]:
|
|
65
62
|
args = get_args(field)
|
|
66
63
|
is_optional = type(None) in args
|
|
67
64
|
for arg in args:
|
|
@@ -70,7 +67,7 @@ def _strip_union(field: Union[Union[T], T]) -> Tuple[T, bool]:
|
|
|
70
67
|
return field, False
|
|
71
68
|
|
|
72
69
|
|
|
73
|
-
def _strip_device_vector(field:
|
|
70
|
+
def _strip_device_vector(field: type[Device]) -> tuple[bool, type[Device]]:
|
|
74
71
|
if get_origin(field) is DeviceVector:
|
|
75
72
|
return True, get_args(field)[0]
|
|
76
73
|
return False, field
|
|
@@ -83,13 +80,13 @@ class _PVIEntry:
|
|
|
83
80
|
This could either be a signal or a sub-table.
|
|
84
81
|
"""
|
|
85
82
|
|
|
86
|
-
sub_entries:
|
|
87
|
-
pvi_pv:
|
|
88
|
-
device:
|
|
89
|
-
common_device_type:
|
|
83
|
+
sub_entries: dict[str, Union[dict[int, "_PVIEntry"], "_PVIEntry"]]
|
|
84
|
+
pvi_pv: str | None = None
|
|
85
|
+
device: Device | None = None
|
|
86
|
+
common_device_type: type[Device] | None = None
|
|
90
87
|
|
|
91
88
|
|
|
92
|
-
def _verify_common_blocks(entry: _PVIEntry, common_device:
|
|
89
|
+
def _verify_common_blocks(entry: _PVIEntry, common_device: type[Device]):
|
|
93
90
|
if not entry.sub_entries:
|
|
94
91
|
return
|
|
95
92
|
common_sub_devices = get_type_hints(common_device)
|
|
@@ -107,12 +104,12 @@ def _verify_common_blocks(entry: _PVIEntry, common_device: Type[Device]):
|
|
|
107
104
|
_verify_common_blocks(sub_sub_entry, sub_device) # type: ignore
|
|
108
105
|
else:
|
|
109
106
|
_verify_common_blocks(
|
|
110
|
-
entry.sub_entries[sub_name],
|
|
107
|
+
entry.sub_entries[sub_name], # type: ignore
|
|
111
108
|
sub_device, # type: ignore
|
|
112
109
|
)
|
|
113
110
|
|
|
114
111
|
|
|
115
|
-
_pvi_mapping:
|
|
112
|
+
_pvi_mapping: dict[frozenset[str], Callable[..., Signal]] = {
|
|
116
113
|
frozenset({"r", "w"}): lambda dtype, read_pv, write_pv: epics_signal_rw(
|
|
117
114
|
dtype, "pva://" + read_pv, "pva://" + write_pv
|
|
118
115
|
),
|
|
@@ -129,8 +126,8 @@ _pvi_mapping: Dict[FrozenSet[str], Callable[..., Signal]] = {
|
|
|
129
126
|
|
|
130
127
|
def _parse_type(
|
|
131
128
|
is_pvi_table: bool,
|
|
132
|
-
number_suffix:
|
|
133
|
-
common_device_type:
|
|
129
|
+
number_suffix: int | None,
|
|
130
|
+
common_device_type: type[Device] | None,
|
|
134
131
|
):
|
|
135
132
|
if common_device_type:
|
|
136
133
|
# pre-defined type
|
|
@@ -159,7 +156,7 @@ def _parse_type(
|
|
|
159
156
|
return is_device_vector, is_signal, signal_dtype, device_cls
|
|
160
157
|
|
|
161
158
|
|
|
162
|
-
def _mock_common_blocks(device: Device, stripped_type:
|
|
159
|
+
def _mock_common_blocks(device: Device, stripped_type: type | None = None):
|
|
163
160
|
device_t = stripped_type or type(device)
|
|
164
161
|
sub_devices = (
|
|
165
162
|
(field, field_type)
|
|
@@ -173,11 +170,10 @@ def _mock_common_blocks(device: Device, stripped_type: Optional[Type] = None):
|
|
|
173
170
|
device_cls, device_args = _split_subscript(device_cls)
|
|
174
171
|
assert issubclass(device_cls, Device)
|
|
175
172
|
|
|
176
|
-
is_signal = issubclass(device_cls, Signal)
|
|
177
173
|
signal_dtype = device_args[0] if device_args is not None else None
|
|
178
174
|
|
|
179
175
|
if is_device_vector:
|
|
180
|
-
if
|
|
176
|
+
if issubclass(device_cls, Signal):
|
|
181
177
|
sub_device_1 = device_cls(SoftSignalBackend(signal_dtype))
|
|
182
178
|
sub_device_2 = device_cls(SoftSignalBackend(signal_dtype))
|
|
183
179
|
sub_device = DeviceVector({1: sub_device_1, 2: sub_device_2})
|
|
@@ -198,7 +194,7 @@ def _mock_common_blocks(device: Device, stripped_type: Optional[Type] = None):
|
|
|
198
194
|
for value in sub_device.values():
|
|
199
195
|
value.parent = sub_device
|
|
200
196
|
else:
|
|
201
|
-
if
|
|
197
|
+
if issubclass(device_cls, Signal):
|
|
202
198
|
sub_device = device_cls(SoftSignalBackend(signal_dtype))
|
|
203
199
|
else:
|
|
204
200
|
sub_device = getattr(device, device_name, device_cls())
|
|
@@ -271,7 +267,8 @@ def _set_device_attributes(entry: _PVIEntry):
|
|
|
271
267
|
# Set the device vector entry to have the device vector as a parent
|
|
272
268
|
device_vector_sub_entry.device.parent = sub_device # type: ignore
|
|
273
269
|
else:
|
|
274
|
-
sub_device = sub_entry.device
|
|
270
|
+
sub_device = sub_entry.device
|
|
271
|
+
assert sub_device, f"Device of {sub_entry} is None"
|
|
275
272
|
if sub_entry.pvi_pv:
|
|
276
273
|
_set_device_attributes(sub_entry)
|
|
277
274
|
|
|
@@ -308,8 +305,8 @@ async def fill_pvi_entries(
|
|
|
308
305
|
|
|
309
306
|
def create_children_from_annotations(
|
|
310
307
|
device: Device,
|
|
311
|
-
included_optional_fields:
|
|
312
|
-
device_vectors:
|
|
308
|
+
included_optional_fields: tuple[str, ...] = (),
|
|
309
|
+
device_vectors: dict[str, int] | None = None,
|
|
313
310
|
):
|
|
314
311
|
"""For intializing blocks at __init__ of ``device``."""
|
|
315
312
|
for name, device_type in get_type_hints(type(device)).items():
|
|
@@ -328,7 +325,7 @@ def create_children_from_annotations(
|
|
|
328
325
|
|
|
329
326
|
if is_device_vector:
|
|
330
327
|
n_device_vector = DeviceVector(
|
|
331
|
-
{i: device_type() for i in range(1, device_vectors[name] + 1)}
|
|
328
|
+
{i: device_type() for i in range(1, device_vectors[name] + 1)} # type: ignore
|
|
332
329
|
)
|
|
333
330
|
setattr(device, name, n_device_vector)
|
|
334
331
|
for sub_device in n_device_vector.values():
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
import logging
|
|
2
3
|
import sys
|
|
4
|
+
from collections.abc import Sequence
|
|
3
5
|
from dataclasses import dataclass
|
|
4
6
|
from enum import Enum
|
|
5
7
|
from math import isnan, nan
|
|
6
|
-
from typing import Any,
|
|
8
|
+
from typing import Any, get_origin
|
|
7
9
|
|
|
8
10
|
import numpy as np
|
|
9
11
|
from aioca import (
|
|
@@ -17,13 +19,16 @@ from aioca import (
|
|
|
17
19
|
caput,
|
|
18
20
|
)
|
|
19
21
|
from aioca.types import AugmentedValue, Dbr, Format
|
|
20
|
-
from bluesky.protocols import
|
|
22
|
+
from bluesky.protocols import Reading
|
|
21
23
|
from epicscorelibs.ca import dbr
|
|
24
|
+
from event_model import DataKey
|
|
25
|
+
from event_model.documents.event_descriptor import Dtype
|
|
22
26
|
|
|
23
27
|
from ophyd_async.core import (
|
|
24
28
|
DEFAULT_TIMEOUT,
|
|
25
29
|
NotConnected,
|
|
26
30
|
ReadingValueCallback,
|
|
31
|
+
RuntimeSubsetEnum,
|
|
27
32
|
SignalBackend,
|
|
28
33
|
T,
|
|
29
34
|
get_dtype,
|
|
@@ -33,7 +38,7 @@ from ophyd_async.core import (
|
|
|
33
38
|
|
|
34
39
|
from ._common import LimitPair, Limits, common_meta, get_supported_values
|
|
35
40
|
|
|
36
|
-
dbr_to_dtype:
|
|
41
|
+
dbr_to_dtype: dict[Dbr, Dtype] = {
|
|
37
42
|
dbr.DBR_STRING: "string",
|
|
38
43
|
dbr.DBR_SHORT: "integer",
|
|
39
44
|
dbr.DBR_FLOAT: "number",
|
|
@@ -46,8 +51,8 @@ dbr_to_dtype: Dict[Dbr, Dtype] = {
|
|
|
46
51
|
def _data_key_from_augmented_value(
|
|
47
52
|
value: AugmentedValue,
|
|
48
53
|
*,
|
|
49
|
-
choices:
|
|
50
|
-
dtype:
|
|
54
|
+
choices: list[str] | None = None,
|
|
55
|
+
dtype: Dtype | None = None,
|
|
51
56
|
) -> DataKey:
|
|
52
57
|
"""Use the return value of get with FORMAT_CTRL to construct a DataKey
|
|
53
58
|
describing the signal. See docstring of AugmentedValue for expected
|
|
@@ -65,14 +70,15 @@ def _data_key_from_augmented_value(
|
|
|
65
70
|
assert value.ok, f"Error reading {source}: {value}"
|
|
66
71
|
|
|
67
72
|
scalar = value.element_count == 1
|
|
68
|
-
dtype = dtype or dbr_to_dtype[value.datatype]
|
|
73
|
+
dtype = dtype or dbr_to_dtype[value.datatype] # type: ignore
|
|
69
74
|
|
|
70
75
|
dtype_numpy = np.dtype(dbr.DbrCodeToType[value.datatype].dtype).descr[0][1]
|
|
71
76
|
|
|
72
77
|
d = DataKey(
|
|
73
78
|
source=source,
|
|
74
79
|
dtype=dtype if scalar else "array",
|
|
75
|
-
|
|
80
|
+
# Ignore until https://github.com/bluesky/event-model/issues/308
|
|
81
|
+
dtype_numpy=dtype_numpy, # type: ignore
|
|
76
82
|
# strictly value.element_count >= len(value)
|
|
77
83
|
shape=[] if scalar else [len(value)],
|
|
78
84
|
)
|
|
@@ -82,10 +88,10 @@ def _data_key_from_augmented_value(
|
|
|
82
88
|
d[key] = attr
|
|
83
89
|
|
|
84
90
|
if choices is not None:
|
|
85
|
-
d["choices"] = choices
|
|
91
|
+
d["choices"] = choices # type: ignore
|
|
86
92
|
|
|
87
93
|
if limits := _limits_from_augmented_value(value):
|
|
88
|
-
d["limits"] = limits
|
|
94
|
+
d["limits"] = limits # type: ignore
|
|
89
95
|
|
|
90
96
|
return d
|
|
91
97
|
|
|
@@ -108,8 +114,8 @@ def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
|
|
|
108
114
|
|
|
109
115
|
@dataclass
|
|
110
116
|
class CaConverter:
|
|
111
|
-
read_dbr:
|
|
112
|
-
write_dbr:
|
|
117
|
+
read_dbr: Dbr | None
|
|
118
|
+
write_dbr: Dbr | None
|
|
113
119
|
|
|
114
120
|
def write_value(self, value) -> Any:
|
|
115
121
|
return value
|
|
@@ -118,9 +124,9 @@ class CaConverter:
|
|
|
118
124
|
# for channel access ca_xxx classes, this
|
|
119
125
|
# invokes __pos__ operator to return an instance of
|
|
120
126
|
# the builtin base class
|
|
121
|
-
return +value
|
|
127
|
+
return +value # type: ignore
|
|
122
128
|
|
|
123
|
-
def reading(self, value: AugmentedValue):
|
|
129
|
+
def reading(self, value: AugmentedValue) -> Reading:
|
|
124
130
|
return {
|
|
125
131
|
"value": self.value(value),
|
|
126
132
|
"timestamp": value.timestamp,
|
|
@@ -155,14 +161,14 @@ class CaEnumConverter(CaConverter):
|
|
|
155
161
|
|
|
156
162
|
choices: dict[str, str]
|
|
157
163
|
|
|
158
|
-
def write_value(self, value:
|
|
164
|
+
def write_value(self, value: Enum | str):
|
|
159
165
|
if isinstance(value, Enum):
|
|
160
166
|
return value.value
|
|
161
167
|
else:
|
|
162
168
|
return value
|
|
163
169
|
|
|
164
170
|
def value(self, value: AugmentedValue):
|
|
165
|
-
return self.choices[value]
|
|
171
|
+
return self.choices[value] # type: ignore
|
|
166
172
|
|
|
167
173
|
def get_datakey(self, value: AugmentedValue) -> DataKey:
|
|
168
174
|
# Sometimes DBR_TYPE returns as String, must pass choices still
|
|
@@ -184,7 +190,7 @@ class DisconnectedCaConverter(CaConverter):
|
|
|
184
190
|
|
|
185
191
|
|
|
186
192
|
def make_converter(
|
|
187
|
-
datatype:
|
|
193
|
+
datatype: type | None, values: dict[str, AugmentedValue]
|
|
188
194
|
) -> CaConverter:
|
|
189
195
|
pv = list(values)[0]
|
|
190
196
|
pv_dbr = get_unique({k: v.datatype for k, v in values.items()}, "datatypes")
|
|
@@ -200,7 +206,7 @@ def make_converter(
|
|
|
200
206
|
raise TypeError(f"{pv} has type [str] not {datatype.__name__}")
|
|
201
207
|
return CaArrayConverter(pv_dbr, None)
|
|
202
208
|
elif is_array:
|
|
203
|
-
pv_dtype = get_unique({k: v.dtype for k, v in values.items()}, "dtypes")
|
|
209
|
+
pv_dtype = get_unique({k: v.dtype for k, v in values.items()}, "dtypes") # type: ignore
|
|
204
210
|
# This is an array
|
|
205
211
|
if datatype:
|
|
206
212
|
# Check we wanted an array of this type
|
|
@@ -209,9 +215,10 @@ def make_converter(
|
|
|
209
215
|
raise TypeError(f"{pv} has type [{pv_dtype}] not {datatype.__name__}")
|
|
210
216
|
if dtype != pv_dtype:
|
|
211
217
|
raise TypeError(f"{pv} has type [{pv_dtype}] not [{dtype}]")
|
|
212
|
-
return CaArrayConverter(pv_dbr, None)
|
|
218
|
+
return CaArrayConverter(pv_dbr, None) # type: ignore
|
|
213
219
|
elif pv_dbr == dbr.DBR_ENUM and datatype is bool:
|
|
214
|
-
# Database can't do bools, so are often representated as enums,
|
|
220
|
+
# Database can't do bools, so are often representated as enums,
|
|
221
|
+
# CA can do int
|
|
215
222
|
pv_choices_len = get_unique(
|
|
216
223
|
{k: len(v.enums) for k, v in values.items()}, "number of choices"
|
|
217
224
|
)
|
|
@@ -240,7 +247,7 @@ def make_converter(
|
|
|
240
247
|
f"{pv} has type {type(value).__name__.replace('ca_', '')} "
|
|
241
248
|
+ f"not {datatype.__name__}"
|
|
242
249
|
)
|
|
243
|
-
|
|
250
|
+
return CaConverter(pv_dbr, None) # type: ignore
|
|
244
251
|
|
|
245
252
|
|
|
246
253
|
_tried_pyepics = False
|
|
@@ -256,13 +263,36 @@ def _use_pyepics_context_if_imported():
|
|
|
256
263
|
|
|
257
264
|
|
|
258
265
|
class CaSignalBackend(SignalBackend[T]):
|
|
259
|
-
|
|
266
|
+
_ALLOWED_DATATYPES = (
|
|
267
|
+
bool,
|
|
268
|
+
int,
|
|
269
|
+
float,
|
|
270
|
+
str,
|
|
271
|
+
Sequence,
|
|
272
|
+
Enum,
|
|
273
|
+
RuntimeSubsetEnum,
|
|
274
|
+
np.ndarray,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def datatype_allowed(cls, dtype: Any) -> bool:
|
|
279
|
+
stripped_origin = get_origin(dtype) or dtype
|
|
280
|
+
if dtype is None:
|
|
281
|
+
return True
|
|
282
|
+
|
|
283
|
+
return inspect.isclass(stripped_origin) and issubclass(
|
|
284
|
+
stripped_origin, cls._ALLOWED_DATATYPES
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def __init__(self, datatype: type[T] | None, read_pv: str, write_pv: str):
|
|
260
288
|
self.datatype = datatype
|
|
289
|
+
if not CaSignalBackend.datatype_allowed(self.datatype):
|
|
290
|
+
raise TypeError(f"Given datatype {self.datatype} unsupported in CA.")
|
|
261
291
|
self.read_pv = read_pv
|
|
262
292
|
self.write_pv = write_pv
|
|
263
|
-
self.initial_values:
|
|
293
|
+
self.initial_values: dict[str, AugmentedValue] = {}
|
|
264
294
|
self.converter: CaConverter = DisconnectedCaConverter(None, None)
|
|
265
|
-
self.subscription:
|
|
295
|
+
self.subscription: Subscription | None = None
|
|
266
296
|
|
|
267
297
|
def source(self, name: str):
|
|
268
298
|
return f"ca://{self.read_pv}"
|
|
@@ -289,7 +319,7 @@ class CaSignalBackend(SignalBackend[T]):
|
|
|
289
319
|
await self._store_initial_value(self.read_pv, timeout=timeout)
|
|
290
320
|
self.converter = make_converter(self.datatype, self.initial_values)
|
|
291
321
|
|
|
292
|
-
async def put(self, value:
|
|
322
|
+
async def put(self, value: T | None, wait=True, timeout=None):
|
|
293
323
|
if value is None:
|
|
294
324
|
write_value = self.initial_values[self.write_pv]
|
|
295
325
|
else:
|
|
@@ -331,7 +361,7 @@ class CaSignalBackend(SignalBackend[T]):
|
|
|
331
361
|
)
|
|
332
362
|
return self.converter.value(value)
|
|
333
363
|
|
|
334
|
-
def set_callback(self, callback:
|
|
364
|
+
def set_callback(self, callback: ReadingValueCallback[T] | None) -> None:
|
|
335
365
|
if callback:
|
|
336
366
|
assert (
|
|
337
367
|
not self.subscription
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from typing import Dict, Optional, Tuple, Type
|
|
4
3
|
|
|
5
4
|
from typing_extensions import TypedDict
|
|
6
5
|
|
|
@@ -16,9 +15,6 @@ class LimitPair(TypedDict):
|
|
|
16
15
|
high: float | None
|
|
17
16
|
low: float | None
|
|
18
17
|
|
|
19
|
-
def __bool__(self) -> bool:
|
|
20
|
-
return self.low is None and self.high is None
|
|
21
|
-
|
|
22
18
|
|
|
23
19
|
class Limits(TypedDict):
|
|
24
20
|
alarm: LimitPair
|
|
@@ -26,15 +22,12 @@ class Limits(TypedDict):
|
|
|
26
22
|
display: LimitPair
|
|
27
23
|
warning: LimitPair
|
|
28
24
|
|
|
29
|
-
def __bool__(self) -> bool:
|
|
30
|
-
return any(self.alarm, self.control, self.display, self.warning)
|
|
31
|
-
|
|
32
25
|
|
|
33
26
|
def get_supported_values(
|
|
34
27
|
pv: str,
|
|
35
|
-
datatype:
|
|
36
|
-
pv_choices:
|
|
37
|
-
) ->
|
|
28
|
+
datatype: type[str] | None,
|
|
29
|
+
pv_choices: tuple[str, ...],
|
|
30
|
+
) -> dict[str, str]:
|
|
38
31
|
if inspect.isclass(datatype) and issubclass(datatype, RuntimeSubsetEnum):
|
|
39
32
|
if not set(datatype.choices).issubset(set(pv_choices)):
|
|
40
33
|
raise TypeError(
|
|
@@ -4,22 +4,25 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from enum import Enum
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
def _make_unavailable_class(error: Exception) -> type:
|
|
9
|
+
class TransportNotAvailable:
|
|
10
|
+
def __init__(*args, **kwargs):
|
|
11
|
+
raise NotImplementedError("Transport not available") from error
|
|
12
|
+
|
|
13
|
+
return TransportNotAvailable
|
|
14
|
+
|
|
15
|
+
|
|
7
16
|
try:
|
|
8
17
|
from ._aioca import CaSignalBackend
|
|
9
18
|
except ImportError as ca_error:
|
|
10
|
-
|
|
11
|
-
class CaSignalBackend: # type: ignore
|
|
12
|
-
def __init__(*args, ca_error=ca_error, **kwargs):
|
|
13
|
-
raise NotImplementedError("CA support not available") from ca_error
|
|
19
|
+
CaSignalBackend = _make_unavailable_class(ca_error)
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
try:
|
|
17
23
|
from ._p4p import PvaSignalBackend
|
|
18
24
|
except ImportError as pva_error:
|
|
19
|
-
|
|
20
|
-
class PvaSignalBackend: # type: ignore
|
|
21
|
-
def __init__(*args, pva_error=pva_error, **kwargs):
|
|
22
|
-
raise NotImplementedError("PVA support not available") from pva_error
|
|
25
|
+
PvaSignalBackend = _make_unavailable_class(pva_error)
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
class _EpicsTransport(Enum):
|