ophyd-async 0.3.4a1__py3-none-any.whl → 0.5.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 +86 -63
- ophyd_async/core/{detector.py → _detector.py} +18 -23
- ophyd_async/core/{device.py → _device.py} +19 -7
- ophyd_async/core/{device_save_loader.py → _device_save_loader.py} +3 -3
- ophyd_async/core/{flyer.py → _flyer.py} +6 -8
- ophyd_async/core/_hdf_dataset.py +97 -0
- ophyd_async/{log.py → core/_log.py} +11 -3
- ophyd_async/core/{mock_signal_backend.py → _mock_signal_backend.py} +3 -3
- ophyd_async/core/{mock_signal_utils.py → _mock_signal_utils.py} +3 -4
- ophyd_async/{protocols.py → core/_protocol.py} +1 -1
- ophyd_async/core/_providers.py +186 -24
- ophyd_async/core/{standard_readable.py → _readable.py} +6 -16
- ophyd_async/core/{signal.py → _signal.py} +39 -16
- ophyd_async/core/{signal_backend.py → _signal_backend.py} +4 -13
- ophyd_async/core/{soft_signal_backend.py → _soft_signal_backend.py} +24 -18
- ophyd_async/core/{async_status.py → _status.py} +3 -11
- ophyd_async/epics/adaravis/__init__.py +9 -0
- ophyd_async/epics/{areadetector/aravis.py → adaravis/_aravis.py} +12 -14
- ophyd_async/epics/{areadetector/controllers/aravis_controller.py → adaravis/_aravis_controller.py} +8 -10
- ophyd_async/epics/{areadetector/drivers/aravis_driver.py → adaravis/_aravis_io.py} +6 -3
- ophyd_async/epics/adcore/__init__.py +36 -0
- ophyd_async/epics/adcore/_core_io.py +114 -0
- ophyd_async/epics/{areadetector/drivers/ad_base.py → adcore/_core_logic.py} +17 -52
- ophyd_async/epics/{areadetector/writers/hdf_writer.py → adcore/_hdf_writer.py} +36 -18
- ophyd_async/epics/{areadetector/single_trigger_det.py → adcore/_single_trigger.py} +5 -6
- ophyd_async/epics/{areadetector/utils.py → adcore/_utils.py} +29 -0
- ophyd_async/epics/adkinetix/__init__.py +9 -0
- ophyd_async/epics/{areadetector/kinetix.py → adkinetix/_kinetix.py} +12 -14
- ophyd_async/epics/{areadetector/controllers/kinetix_controller.py → adkinetix/_kinetix_controller.py} +6 -9
- ophyd_async/epics/{areadetector/drivers/kinetix_driver.py → adkinetix/_kinetix_io.py} +5 -4
- ophyd_async/epics/adpilatus/__init__.py +11 -0
- ophyd_async/epics/{areadetector/pilatus.py → adpilatus/_pilatus.py} +12 -16
- ophyd_async/epics/{areadetector/controllers/pilatus_controller.py → adpilatus/_pilatus_controller.py} +14 -16
- ophyd_async/epics/{areadetector/drivers/pilatus_driver.py → adpilatus/_pilatus_io.py} +5 -3
- ophyd_async/epics/adsimdetector/__init__.py +7 -0
- ophyd_async/epics/adsimdetector/_sim.py +34 -0
- ophyd_async/epics/{areadetector/controllers/ad_sim_controller.py → adsimdetector/_sim_controller.py} +8 -14
- ophyd_async/epics/advimba/__init__.py +9 -0
- ophyd_async/epics/advimba/_vimba.py +43 -0
- ophyd_async/epics/{areadetector/controllers/vimba_controller.py → advimba/_vimba_controller.py} +6 -14
- ophyd_async/epics/{areadetector/drivers/vimba_driver.py → advimba/_vimba_io.py} +5 -4
- ophyd_async/epics/demo/__init__.py +9 -132
- ophyd_async/epics/demo/_mover.py +97 -0
- ophyd_async/epics/demo/_sensor.py +36 -0
- ophyd_async/epics/motor.py +228 -0
- ophyd_async/epics/pvi/__init__.py +2 -2
- ophyd_async/epics/pvi/{pvi.py → _pvi.py} +17 -14
- ophyd_async/epics/signal/__init__.py +7 -1
- ophyd_async/epics/{_backend → signal}/_aioca.py +6 -2
- ophyd_async/epics/{_backend/common.py → signal/_common.py} +4 -2
- ophyd_async/epics/signal/_epics_transport.py +3 -3
- ophyd_async/epics/{_backend → signal}/_p4p.py +53 -4
- ophyd_async/epics/signal/{signal.py → _signal.py} +10 -9
- ophyd_async/fastcs/odin/__init__.py +0 -0
- ophyd_async/{panda → fastcs/panda}/__init__.py +28 -9
- ophyd_async/{panda → fastcs/panda}/_common_blocks.py +24 -3
- ophyd_async/{panda → fastcs/panda}/_hdf_panda.py +6 -9
- ophyd_async/{panda/writers → fastcs/panda}/_hdf_writer.py +24 -14
- ophyd_async/{panda → fastcs/panda}/_panda_controller.py +2 -1
- ophyd_async/{panda → fastcs/panda}/_table.py +20 -18
- ophyd_async/fastcs/panda/_trigger.py +90 -0
- ophyd_async/plan_stubs/__init__.py +2 -2
- ophyd_async/plan_stubs/_ensure_connected.py +26 -0
- ophyd_async/plan_stubs/{fly.py → _fly.py} +67 -12
- ophyd_async/sim/__init__.py +0 -11
- ophyd_async/sim/demo/__init__.py +18 -2
- ophyd_async/sim/demo/_pattern_detector/__init__.py +13 -0
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +42 -0
- ophyd_async/sim/{sim_pattern_detector_control.py → demo/_pattern_detector/_pattern_detector_controller.py} +6 -7
- ophyd_async/sim/{sim_pattern_detector_writer.py → demo/_pattern_detector/_pattern_detector_writer.py} +12 -8
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +211 -0
- ophyd_async/sim/demo/{sim_motor.py → _sim_motor.py} +7 -5
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async/tango/__init__.py +0 -0
- {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.5.0.dist-info}/METADATA +7 -2
- ophyd_async-0.5.0.dist-info/RECORD +89 -0
- {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.5.0.dist-info}/WHEEL +1 -1
- ophyd_async/epics/areadetector/__init__.py +0 -23
- ophyd_async/epics/areadetector/controllers/__init__.py +0 -5
- ophyd_async/epics/areadetector/drivers/__init__.py +0 -23
- ophyd_async/epics/areadetector/vimba.py +0 -43
- ophyd_async/epics/areadetector/writers/__init__.py +0 -5
- ophyd_async/epics/areadetector/writers/_hdfdataset.py +0 -10
- ophyd_async/epics/areadetector/writers/_hdffile.py +0 -54
- ophyd_async/epics/areadetector/writers/nd_file_hdf.py +0 -40
- ophyd_async/epics/areadetector/writers/nd_plugin.py +0 -38
- ophyd_async/epics/demo/demo_ad_sim_detector.py +0 -35
- ophyd_async/epics/motion/__init__.py +0 -3
- ophyd_async/epics/motion/motor.py +0 -97
- ophyd_async/panda/_trigger.py +0 -39
- ophyd_async/panda/writers/__init__.py +0 -3
- ophyd_async/panda/writers/_panda_hdf_file.py +0 -54
- ophyd_async/plan_stubs/ensure_connected.py +0 -22
- ophyd_async/sim/pattern_generator.py +0 -318
- ophyd_async/sim/sim_pattern_generator.py +0 -35
- ophyd_async-0.3.4a1.dist-info/RECORD +0 -86
- /ophyd_async/core/{utils.py → _utils.py} +0 -0
- /ophyd_async/{epics/_backend → fastcs}/__init__.py +0 -0
- /ophyd_async/{panda → fastcs/panda}/_utils.py +0 -0
- {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.5.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.5.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.3.4a1.dist-info → ophyd_async-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from bluesky.protocols import Flyable, Movable, Preparable, Stoppable
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from ophyd_async.core import (
|
|
8
|
+
DEFAULT_TIMEOUT,
|
|
9
|
+
AsyncStatus,
|
|
10
|
+
CalculatableTimeout,
|
|
11
|
+
CalculateTimeout,
|
|
12
|
+
ConfigSignal,
|
|
13
|
+
HintedSignal,
|
|
14
|
+
StandardReadable,
|
|
15
|
+
WatchableAsyncStatus,
|
|
16
|
+
WatcherUpdate,
|
|
17
|
+
observe_value,
|
|
18
|
+
)
|
|
19
|
+
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MotorLimitsException(Exception):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InvalidFlyMotorException(Exception):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
DEFAULT_MOTOR_FLY_TIMEOUT = 60
|
|
31
|
+
DEFAULT_WATCHER_UPDATE_FREQUENCY = 0.2
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FlyMotorInfo(BaseModel):
|
|
35
|
+
"""Minimal set of information required to fly a motor:"""
|
|
36
|
+
|
|
37
|
+
#: Absolute position of the motor once it finishes accelerating to desired
|
|
38
|
+
#: velocity, in motor EGUs
|
|
39
|
+
start_position: float = Field(frozen=True)
|
|
40
|
+
|
|
41
|
+
#: Absolute position of the motor once it begins decelerating from desired
|
|
42
|
+
#: velocity, in EGUs
|
|
43
|
+
end_position: float = Field(frozen=True)
|
|
44
|
+
|
|
45
|
+
#: Time taken for the motor to get from start_position to end_position, excluding
|
|
46
|
+
#: run-up and run-down, in seconds.
|
|
47
|
+
time_for_move: float = Field(frozen=True, gt=0)
|
|
48
|
+
|
|
49
|
+
#: Maximum time for the complete motor move, including run up and run down.
|
|
50
|
+
#: Defaults to `time_for_move` + run up and run down times + 10s.
|
|
51
|
+
timeout: CalculatableTimeout = Field(frozen=True, default=CalculateTimeout)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Motor(StandardReadable, Movable, Stoppable, Flyable, Preparable):
|
|
55
|
+
"""Device that moves a motor record"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, prefix: str, name="") -> None:
|
|
58
|
+
# Define some signals
|
|
59
|
+
with self.add_children_as_readables(ConfigSignal):
|
|
60
|
+
self.motor_egu = epics_signal_r(str, prefix + ".EGU")
|
|
61
|
+
self.velocity = epics_signal_rw(float, prefix + ".VELO")
|
|
62
|
+
|
|
63
|
+
with self.add_children_as_readables(HintedSignal):
|
|
64
|
+
self.user_readback = epics_signal_r(float, prefix + ".RBV")
|
|
65
|
+
|
|
66
|
+
self.user_setpoint = epics_signal_rw(float, prefix + ".VAL")
|
|
67
|
+
self.max_velocity = epics_signal_r(float, prefix + ".VMAX")
|
|
68
|
+
self.acceleration_time = epics_signal_rw(float, prefix + ".ACCL")
|
|
69
|
+
self.precision = epics_signal_r(int, prefix + ".PREC")
|
|
70
|
+
self.deadband = epics_signal_r(float, prefix + ".RDBD")
|
|
71
|
+
self.motor_done_move = epics_signal_r(int, prefix + ".DMOV")
|
|
72
|
+
self.low_limit_travel = epics_signal_rw(float, prefix + ".LLM")
|
|
73
|
+
self.high_limit_travel = epics_signal_rw(float, prefix + ".HLM")
|
|
74
|
+
|
|
75
|
+
self.motor_stop = epics_signal_x(prefix + ".STOP")
|
|
76
|
+
# Whether set() should complete successfully or not
|
|
77
|
+
self._set_success = True
|
|
78
|
+
|
|
79
|
+
# end_position of a fly move, with run_up_distance added on.
|
|
80
|
+
self._fly_completed_position: Optional[float] = None
|
|
81
|
+
|
|
82
|
+
# Set on kickoff(), complete when motor reaches self._fly_completed_position
|
|
83
|
+
self._fly_status: Optional[WatchableAsyncStatus] = None
|
|
84
|
+
|
|
85
|
+
# Set during prepare
|
|
86
|
+
self._fly_timeout: Optional[CalculatableTimeout] = CalculateTimeout
|
|
87
|
+
|
|
88
|
+
super().__init__(name=name)
|
|
89
|
+
|
|
90
|
+
def set_name(self, name: str):
|
|
91
|
+
super().set_name(name)
|
|
92
|
+
# Readback should be named the same as its parent in read()
|
|
93
|
+
self.user_readback.set_name(name)
|
|
94
|
+
|
|
95
|
+
@AsyncStatus.wrap
|
|
96
|
+
async def prepare(self, value: FlyMotorInfo):
|
|
97
|
+
"""Calculate required velocity and run-up distance, then if motor limits aren't
|
|
98
|
+
breached, move to start position minus run-up distance"""
|
|
99
|
+
|
|
100
|
+
self._fly_timeout = value.timeout
|
|
101
|
+
|
|
102
|
+
# Velocity, at which motor travels from start_position to end_position, in motor
|
|
103
|
+
# egu/s.
|
|
104
|
+
fly_velocity = await self._prepare_velocity(
|
|
105
|
+
value.start_position,
|
|
106
|
+
value.end_position,
|
|
107
|
+
value.time_for_move,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# start_position with run_up_distance added on.
|
|
111
|
+
fly_prepared_position = await self._prepare_motor_path(
|
|
112
|
+
abs(fly_velocity), value.start_position, value.end_position
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
await self.set(fly_prepared_position)
|
|
116
|
+
await self.velocity.set(fly_velocity)
|
|
117
|
+
|
|
118
|
+
@AsyncStatus.wrap
|
|
119
|
+
async def kickoff(self):
|
|
120
|
+
"""Begin moving motor from prepared position to final position."""
|
|
121
|
+
assert (
|
|
122
|
+
self._fly_completed_position
|
|
123
|
+
), "Motor must be prepared before attempting to kickoff"
|
|
124
|
+
|
|
125
|
+
self._fly_status = self.set(
|
|
126
|
+
self._fly_completed_position, timeout=self._fly_timeout
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def complete(self) -> WatchableAsyncStatus:
|
|
130
|
+
"""Mark as complete once motor reaches completed position."""
|
|
131
|
+
assert self._fly_status, "kickoff not called"
|
|
132
|
+
return self._fly_status
|
|
133
|
+
|
|
134
|
+
@WatchableAsyncStatus.wrap
|
|
135
|
+
async def set(
|
|
136
|
+
self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
|
|
137
|
+
):
|
|
138
|
+
self._set_success = True
|
|
139
|
+
(
|
|
140
|
+
old_position,
|
|
141
|
+
units,
|
|
142
|
+
precision,
|
|
143
|
+
velocity,
|
|
144
|
+
acceleration_time,
|
|
145
|
+
) = await asyncio.gather(
|
|
146
|
+
self.user_setpoint.get_value(),
|
|
147
|
+
self.motor_egu.get_value(),
|
|
148
|
+
self.precision.get_value(),
|
|
149
|
+
self.velocity.get_value(),
|
|
150
|
+
self.acceleration_time.get_value(),
|
|
151
|
+
)
|
|
152
|
+
if timeout is CalculateTimeout:
|
|
153
|
+
assert velocity > 0, "Motor has zero velocity"
|
|
154
|
+
timeout = (
|
|
155
|
+
abs(new_position - old_position) / velocity
|
|
156
|
+
+ 2 * acceleration_time
|
|
157
|
+
+ DEFAULT_TIMEOUT
|
|
158
|
+
)
|
|
159
|
+
move_status = self.user_setpoint.set(new_position, wait=True, timeout=timeout)
|
|
160
|
+
async for current_position in observe_value(
|
|
161
|
+
self.user_readback, done_status=move_status
|
|
162
|
+
):
|
|
163
|
+
yield WatcherUpdate(
|
|
164
|
+
current=current_position,
|
|
165
|
+
initial=old_position,
|
|
166
|
+
target=new_position,
|
|
167
|
+
name=self.name,
|
|
168
|
+
unit=units,
|
|
169
|
+
precision=precision,
|
|
170
|
+
)
|
|
171
|
+
if not self._set_success:
|
|
172
|
+
raise RuntimeError("Motor was stopped")
|
|
173
|
+
|
|
174
|
+
async def stop(self, success=False):
|
|
175
|
+
self._set_success = success
|
|
176
|
+
# Put with completion will never complete as we are waiting for completion on
|
|
177
|
+
# the move above, so need to pass wait=False
|
|
178
|
+
await self.motor_stop.trigger(wait=False)
|
|
179
|
+
|
|
180
|
+
async def _prepare_velocity(
|
|
181
|
+
self, start_position: float, end_position: float, time_for_move: float
|
|
182
|
+
) -> float:
|
|
183
|
+
fly_velocity = (end_position - start_position) / time_for_move
|
|
184
|
+
max_speed, egu = await asyncio.gather(
|
|
185
|
+
self.max_velocity.get_value(), self.motor_egu.get_value()
|
|
186
|
+
)
|
|
187
|
+
if abs(fly_velocity) > max_speed:
|
|
188
|
+
raise MotorLimitsException(
|
|
189
|
+
f"Motor speed of {abs(fly_velocity)} {egu}/s was requested for a motor "
|
|
190
|
+
f" with max speed of {max_speed} {egu}/s"
|
|
191
|
+
)
|
|
192
|
+
# move to prepare position at maximum velocity
|
|
193
|
+
await self.velocity.set(abs(max_speed))
|
|
194
|
+
return fly_velocity
|
|
195
|
+
|
|
196
|
+
async def _prepare_motor_path(
|
|
197
|
+
self, fly_velocity: float, start_position: float, end_position: float
|
|
198
|
+
) -> float:
|
|
199
|
+
# Distance required for motor to accelerate from stationary to fly_velocity, and
|
|
200
|
+
# distance required for motor to decelerate from fly_velocity to stationary
|
|
201
|
+
run_up_distance = (
|
|
202
|
+
(await self.acceleration_time.get_value()) * fly_velocity * 0.5
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
self._fly_completed_position = end_position + run_up_distance
|
|
206
|
+
|
|
207
|
+
# Prepared position not used after prepare, so no need to store in self
|
|
208
|
+
fly_prepared_position = start_position - run_up_distance
|
|
209
|
+
|
|
210
|
+
motor_lower_limit, motor_upper_limit, egu = await asyncio.gather(
|
|
211
|
+
self.low_limit_travel.get_value(),
|
|
212
|
+
self.high_limit_travel.get_value(),
|
|
213
|
+
self.motor_egu.get_value(),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if (
|
|
217
|
+
not motor_upper_limit >= fly_prepared_position >= motor_lower_limit
|
|
218
|
+
or not motor_upper_limit
|
|
219
|
+
>= self._fly_completed_position
|
|
220
|
+
>= motor_lower_limit
|
|
221
|
+
):
|
|
222
|
+
raise MotorLimitsException(
|
|
223
|
+
f"Motor trajectory for requested fly is from "
|
|
224
|
+
f"{fly_prepared_position}{egu} to "
|
|
225
|
+
f"{self._fly_completed_position}{egu} but motor limits are "
|
|
226
|
+
f"{motor_lower_limit}{egu} <= x <= {motor_upper_limit}{egu} "
|
|
227
|
+
)
|
|
228
|
+
return fly_prepared_position
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from ._pvi import create_children_from_annotations, fill_pvi_entries
|
|
2
2
|
|
|
3
|
-
__all__ = ["
|
|
3
|
+
__all__ = ["fill_pvi_entries", "create_children_from_annotations"]
|
|
@@ -10,25 +10,28 @@ from typing import (
|
|
|
10
10
|
Optional,
|
|
11
11
|
Tuple,
|
|
12
12
|
Type,
|
|
13
|
-
TypeVar,
|
|
14
13
|
Union,
|
|
15
14
|
get_args,
|
|
16
15
|
get_origin,
|
|
17
16
|
get_type_hints,
|
|
18
17
|
)
|
|
19
18
|
|
|
20
|
-
from ophyd_async.core import
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
from ophyd_async.core import (
|
|
20
|
+
DEFAULT_TIMEOUT,
|
|
21
|
+
Device,
|
|
22
|
+
DeviceVector,
|
|
23
|
+
Signal,
|
|
24
|
+
SoftSignalBackend,
|
|
25
|
+
T,
|
|
26
|
+
)
|
|
27
|
+
from ophyd_async.epics.signal import (
|
|
28
|
+
PvaSignalBackend,
|
|
25
29
|
epics_signal_r,
|
|
26
30
|
epics_signal_rw,
|
|
27
31
|
epics_signal_w,
|
|
28
32
|
epics_signal_x,
|
|
29
33
|
)
|
|
30
34
|
|
|
31
|
-
T = TypeVar("T")
|
|
32
35
|
Access = FrozenSet[
|
|
33
36
|
Union[Literal["r"], Literal["w"], Literal["rw"], Literal["x"], Literal["d"]]
|
|
34
37
|
]
|
|
@@ -74,19 +77,19 @@ def _strip_device_vector(field: Union[Type[Device]]) -> Tuple[bool, Type[Device]
|
|
|
74
77
|
|
|
75
78
|
|
|
76
79
|
@dataclass
|
|
77
|
-
class
|
|
80
|
+
class _PVIEntry:
|
|
78
81
|
"""
|
|
79
82
|
A dataclass to represent a single entry in the PVI table.
|
|
80
83
|
This could either be a signal or a sub-table.
|
|
81
84
|
"""
|
|
82
85
|
|
|
83
|
-
sub_entries: Dict[str, Union[Dict[int, "
|
|
86
|
+
sub_entries: Dict[str, Union[Dict[int, "_PVIEntry"], "_PVIEntry"]]
|
|
84
87
|
pvi_pv: Optional[str] = None
|
|
85
88
|
device: Optional[Device] = None
|
|
86
89
|
common_device_type: Optional[Type[Device]] = None
|
|
87
90
|
|
|
88
91
|
|
|
89
|
-
def _verify_common_blocks(entry:
|
|
92
|
+
def _verify_common_blocks(entry: _PVIEntry, common_device: Type[Device]):
|
|
90
93
|
if not entry.sub_entries:
|
|
91
94
|
return
|
|
92
95
|
common_sub_devices = get_type_hints(common_device)
|
|
@@ -205,7 +208,7 @@ def _mock_common_blocks(device: Device, stripped_type: Optional[Type] = None):
|
|
|
205
208
|
sub_device.parent = device
|
|
206
209
|
|
|
207
210
|
|
|
208
|
-
async def _get_pvi_entries(entry:
|
|
211
|
+
async def _get_pvi_entries(entry: _PVIEntry, timeout=DEFAULT_TIMEOUT):
|
|
209
212
|
if not entry.pvi_pv or not entry.pvi_pv.endswith(":PVI"):
|
|
210
213
|
raise RuntimeError("Top level entry must be a pvi table")
|
|
211
214
|
|
|
@@ -235,7 +238,7 @@ async def _get_pvi_entries(entry: PVIEntry, timeout=DEFAULT_TIMEOUT):
|
|
|
235
238
|
else:
|
|
236
239
|
device = getattr(entry.device, sub_name, device_type())
|
|
237
240
|
|
|
238
|
-
sub_entry =
|
|
241
|
+
sub_entry = _PVIEntry(
|
|
239
242
|
device=device, common_device_type=device_type, sub_entries={}
|
|
240
243
|
)
|
|
241
244
|
|
|
@@ -257,7 +260,7 @@ async def _get_pvi_entries(entry: PVIEntry, timeout=DEFAULT_TIMEOUT):
|
|
|
257
260
|
_verify_common_blocks(entry, entry.common_device_type)
|
|
258
261
|
|
|
259
262
|
|
|
260
|
-
def _set_device_attributes(entry:
|
|
263
|
+
def _set_device_attributes(entry: _PVIEntry):
|
|
261
264
|
for sub_name, sub_entry in entry.sub_entries.items():
|
|
262
265
|
if isinstance(sub_entry, dict):
|
|
263
266
|
sub_device = DeviceVector() # type: ignore
|
|
@@ -289,7 +292,7 @@ async def fill_pvi_entries(
|
|
|
289
292
|
_mock_common_blocks(device)
|
|
290
293
|
else:
|
|
291
294
|
# check the pvi table for devices and fill the device with them
|
|
292
|
-
root_entry =
|
|
295
|
+
root_entry = _PVIEntry(
|
|
293
296
|
pvi_pv=root_pv,
|
|
294
297
|
device=device,
|
|
295
298
|
common_device_type=type(device),
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from ._common import LimitPair, Limits, get_supported_values
|
|
2
|
+
from ._p4p import PvaSignalBackend
|
|
3
|
+
from ._signal import (
|
|
2
4
|
epics_signal_r,
|
|
3
5
|
epics_signal_rw,
|
|
4
6
|
epics_signal_rw_rbv,
|
|
@@ -7,6 +9,10 @@ from .signal import (
|
|
|
7
9
|
)
|
|
8
10
|
|
|
9
11
|
__all__ = [
|
|
12
|
+
"get_supported_values",
|
|
13
|
+
"LimitPair",
|
|
14
|
+
"Limits",
|
|
15
|
+
"PvaSignalBackend",
|
|
10
16
|
"epics_signal_r",
|
|
11
17
|
"epics_signal_rw",
|
|
12
18
|
"epics_signal_rw_rbv",
|
|
@@ -21,6 +21,8 @@ from bluesky.protocols import DataKey, Dtype, Reading
|
|
|
21
21
|
from epicscorelibs.ca import dbr
|
|
22
22
|
|
|
23
23
|
from ophyd_async.core import (
|
|
24
|
+
DEFAULT_TIMEOUT,
|
|
25
|
+
NotConnected,
|
|
24
26
|
ReadingValueCallback,
|
|
25
27
|
SignalBackend,
|
|
26
28
|
T,
|
|
@@ -28,9 +30,8 @@ from ophyd_async.core import (
|
|
|
28
30
|
get_unique,
|
|
29
31
|
wait_for_connection,
|
|
30
32
|
)
|
|
31
|
-
from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected
|
|
32
33
|
|
|
33
|
-
from .
|
|
34
|
+
from ._common import LimitPair, Limits, common_meta, get_supported_values
|
|
34
35
|
|
|
35
36
|
dbr_to_dtype: Dict[Dbr, Dtype] = {
|
|
36
37
|
dbr.DBR_STRING: "string",
|
|
@@ -66,9 +67,12 @@ def _data_key_from_augmented_value(
|
|
|
66
67
|
scalar = value.element_count == 1
|
|
67
68
|
dtype = dtype or dbr_to_dtype[value.datatype]
|
|
68
69
|
|
|
70
|
+
dtype_numpy = np.dtype(dbr.DbrCodeToType[value.datatype].dtype).descr[0][1]
|
|
71
|
+
|
|
69
72
|
d = DataKey(
|
|
70
73
|
source=source,
|
|
71
74
|
dtype=dtype if scalar else "array",
|
|
75
|
+
dtype_numpy=dtype_numpy,
|
|
72
76
|
# strictly value.element_count >= len(value)
|
|
73
77
|
shape=[] if scalar else [len(value)],
|
|
74
78
|
)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from typing import Dict, Optional, Tuple, Type
|
|
3
|
+
from typing import Dict, Optional, Tuple, Type
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from typing_extensions import TypedDict
|
|
6
|
+
|
|
7
|
+
from ophyd_async.core import RuntimeSubsetEnum
|
|
6
8
|
|
|
7
9
|
common_meta = {
|
|
8
10
|
"units",
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from enum import Enum
|
|
6
6
|
|
|
7
7
|
try:
|
|
8
|
-
from
|
|
8
|
+
from ._aioca import CaSignalBackend
|
|
9
9
|
except ImportError as ca_error:
|
|
10
10
|
|
|
11
11
|
class CaSignalBackend: # type: ignore
|
|
@@ -14,7 +14,7 @@ except ImportError as ca_error:
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
try:
|
|
17
|
-
from
|
|
17
|
+
from ._p4p import PvaSignalBackend
|
|
18
18
|
except ImportError as pva_error:
|
|
19
19
|
|
|
20
20
|
class PvaSignalBackend: # type: ignore
|
|
@@ -22,7 +22,7 @@ except ImportError as pva_error:
|
|
|
22
22
|
raise NotImplementedError("PVA support not available") from pva_error
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
class
|
|
25
|
+
class _EpicsTransport(Enum):
|
|
26
26
|
"""The sorts of transport EPICS support"""
|
|
27
27
|
|
|
28
28
|
#: Use Channel Access (using aioca library)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import atexit
|
|
3
|
+
import inspect
|
|
3
4
|
import logging
|
|
4
5
|
import time
|
|
5
6
|
from dataclasses import dataclass
|
|
@@ -12,16 +13,18 @@ from p4p import Value
|
|
|
12
13
|
from p4p.client.asyncio import Context, Subscription
|
|
13
14
|
|
|
14
15
|
from ophyd_async.core import (
|
|
16
|
+
DEFAULT_TIMEOUT,
|
|
17
|
+
NotConnected,
|
|
15
18
|
ReadingValueCallback,
|
|
19
|
+
RuntimeSubsetEnum,
|
|
16
20
|
SignalBackend,
|
|
17
21
|
T,
|
|
18
22
|
get_dtype,
|
|
19
23
|
get_unique,
|
|
20
24
|
wait_for_connection,
|
|
21
25
|
)
|
|
22
|
-
from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected
|
|
23
26
|
|
|
24
|
-
from .
|
|
27
|
+
from ._common import LimitPair, Limits, common_meta, get_supported_values
|
|
25
28
|
|
|
26
29
|
# https://mdavidsaver.github.io/p4p/values.html
|
|
27
30
|
specifier_to_dtype: Dict[str, Dtype] = {
|
|
@@ -39,6 +42,21 @@ specifier_to_dtype: Dict[str, Dtype] = {
|
|
|
39
42
|
"s": "string",
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
specifier_to_np_dtype: Dict[str, str] = {
|
|
46
|
+
"?": "<i2", # bool
|
|
47
|
+
"b": "|i1", # int8
|
|
48
|
+
"B": "|u1", # uint8
|
|
49
|
+
"h": "<i2", # int16
|
|
50
|
+
"H": "<u2", # uint16
|
|
51
|
+
"i": "<i4", # int32
|
|
52
|
+
"I": "<u4", # uint32
|
|
53
|
+
"l": "<i8", # int64
|
|
54
|
+
"L": "<u8", # uint64
|
|
55
|
+
"f": "<f4", # float32
|
|
56
|
+
"d": "<f8", # float64
|
|
57
|
+
"s": "|S40",
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
|
|
43
61
|
def _data_key_from_value(
|
|
44
62
|
source: str,
|
|
@@ -59,12 +77,35 @@ def _data_key_from_value(
|
|
|
59
77
|
DataKey: A rich DataKey describing the DB record
|
|
60
78
|
"""
|
|
61
79
|
shape = shape or []
|
|
62
|
-
|
|
80
|
+
type_code = value.type().aspy("value")
|
|
81
|
+
|
|
82
|
+
dtype = dtype or specifier_to_dtype[type_code]
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
if isinstance(type_code, tuple):
|
|
86
|
+
dtype_numpy = ""
|
|
87
|
+
if type_code[1] == "enum_t":
|
|
88
|
+
if dtype == "bool":
|
|
89
|
+
dtype_numpy = "<i2"
|
|
90
|
+
else:
|
|
91
|
+
for item in type_code[2]:
|
|
92
|
+
if item[0] == "choices":
|
|
93
|
+
dtype_numpy = specifier_to_np_dtype[item[1][1]]
|
|
94
|
+
elif not type_code.startswith("a"):
|
|
95
|
+
dtype_numpy = specifier_to_np_dtype[type_code]
|
|
96
|
+
else:
|
|
97
|
+
# Array type, use typecode of internal element
|
|
98
|
+
dtype_numpy = specifier_to_np_dtype[type_code[1]]
|
|
99
|
+
except KeyError:
|
|
100
|
+
# Case where we can't determine dtype string from value
|
|
101
|
+
dtype_numpy = ""
|
|
102
|
+
|
|
63
103
|
display_data = getattr(value, "display", None)
|
|
64
104
|
|
|
65
105
|
d = DataKey(
|
|
66
106
|
source=source,
|
|
67
107
|
dtype=dtype,
|
|
108
|
+
dtype_numpy=dtype_numpy,
|
|
68
109
|
shape=shape,
|
|
69
110
|
)
|
|
70
111
|
if display_data is not None:
|
|
@@ -249,7 +290,7 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
|
|
|
249
290
|
typ = get_unique(
|
|
250
291
|
{k: type(v.get("value")) for k, v in values.items()}, "value types"
|
|
251
292
|
)
|
|
252
|
-
if "NTScalarArray" in typeid and typ
|
|
293
|
+
if "NTScalarArray" in typeid and typ is list:
|
|
253
294
|
# Waveform of strings, check we wanted this
|
|
254
295
|
if datatype and datatype != Sequence[str]:
|
|
255
296
|
raise TypeError(f"{pv} has type [str] not {datatype.__name__}")
|
|
@@ -287,6 +328,14 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
|
|
|
287
328
|
return PvaEnumConverter(get_supported_values(pv, datatype, pv_choices))
|
|
288
329
|
elif "NTScalar" in typeid:
|
|
289
330
|
if (
|
|
331
|
+
typ is str
|
|
332
|
+
and inspect.isclass(datatype)
|
|
333
|
+
and issubclass(datatype, RuntimeSubsetEnum)
|
|
334
|
+
):
|
|
335
|
+
return PvaEnumConverter(
|
|
336
|
+
get_supported_values(pv, datatype, datatype.choices)
|
|
337
|
+
)
|
|
338
|
+
elif (
|
|
290
339
|
datatype
|
|
291
340
|
and not issubclass(typ, datatype)
|
|
292
341
|
and not (
|
|
@@ -14,26 +14,27 @@ from ophyd_async.core import (
|
|
|
14
14
|
get_unique,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
from ._epics_transport import
|
|
17
|
+
from ._epics_transport import _EpicsTransport
|
|
18
18
|
|
|
19
|
-
_default_epics_transport =
|
|
19
|
+
_default_epics_transport = _EpicsTransport.ca
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def _transport_pv(pv: str) -> Tuple[
|
|
22
|
+
def _transport_pv(pv: str) -> Tuple[_EpicsTransport, str]:
|
|
23
23
|
split = pv.split("://", 1)
|
|
24
24
|
if len(split) > 1:
|
|
25
25
|
# We got something like pva://mydevice, so use specified comms mode
|
|
26
26
|
transport_str, pv = split
|
|
27
|
-
transport =
|
|
27
|
+
transport = _EpicsTransport[transport_str]
|
|
28
28
|
else:
|
|
29
29
|
# No comms mode specified, use the default
|
|
30
30
|
transport = _default_epics_transport
|
|
31
31
|
return transport, pv
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
def
|
|
34
|
+
def _epics_signal_backend(
|
|
35
35
|
datatype: Optional[Type[T]], read_pv: str, write_pv: str
|
|
36
36
|
) -> SignalBackend[T]:
|
|
37
|
+
"""Create an epics signal backend."""
|
|
37
38
|
r_transport, r_pv = _transport_pv(read_pv)
|
|
38
39
|
w_transport, w_pv = _transport_pv(write_pv)
|
|
39
40
|
transport = get_unique({read_pv: r_transport, write_pv: w_transport}, "transports")
|
|
@@ -54,7 +55,7 @@ def epics_signal_rw(
|
|
|
54
55
|
write_pv:
|
|
55
56
|
If given, use this PV to write to, otherwise use read_pv
|
|
56
57
|
"""
|
|
57
|
-
backend =
|
|
58
|
+
backend = _epics_signal_backend(datatype, read_pv, write_pv or read_pv)
|
|
58
59
|
return SignalRW(backend, name=name)
|
|
59
60
|
|
|
60
61
|
|
|
@@ -85,7 +86,7 @@ def epics_signal_r(datatype: Type[T], read_pv: str, name: str = "") -> SignalR[T
|
|
|
85
86
|
read_pv:
|
|
86
87
|
The PV to read and monitor
|
|
87
88
|
"""
|
|
88
|
-
backend =
|
|
89
|
+
backend = _epics_signal_backend(datatype, read_pv, read_pv)
|
|
89
90
|
return SignalR(backend, name=name)
|
|
90
91
|
|
|
91
92
|
|
|
@@ -99,7 +100,7 @@ def epics_signal_w(datatype: Type[T], write_pv: str, name: str = "") -> SignalW[
|
|
|
99
100
|
write_pv:
|
|
100
101
|
The PV to write to
|
|
101
102
|
"""
|
|
102
|
-
backend =
|
|
103
|
+
backend = _epics_signal_backend(datatype, write_pv, write_pv)
|
|
103
104
|
return SignalW(backend, name=name)
|
|
104
105
|
|
|
105
106
|
|
|
@@ -111,5 +112,5 @@ def epics_signal_x(write_pv: str, name: str = "") -> SignalX:
|
|
|
111
112
|
write_pv:
|
|
112
113
|
The PV to write its initial value to on trigger
|
|
113
114
|
"""
|
|
114
|
-
backend: SignalBackend =
|
|
115
|
+
backend: SignalBackend = _epics_signal_backend(None, write_pv, write_pv)
|
|
115
116
|
return SignalX(backend, name=name)
|
|
File without changes
|
|
@@ -1,38 +1,57 @@
|
|
|
1
1
|
from ._common_blocks import (
|
|
2
2
|
CommonPandaBlocks,
|
|
3
3
|
DataBlock,
|
|
4
|
+
EnableDisableOptions,
|
|
4
5
|
PcapBlock,
|
|
6
|
+
PcompBlock,
|
|
7
|
+
PcompDirectionOptions,
|
|
5
8
|
PulseBlock,
|
|
6
9
|
SeqBlock,
|
|
7
10
|
TimeUnits,
|
|
8
11
|
)
|
|
9
12
|
from ._hdf_panda import HDFPanda
|
|
13
|
+
from ._hdf_writer import PandaHDFWriter
|
|
10
14
|
from ._panda_controller import PandaPcapController
|
|
11
15
|
from ._table import (
|
|
16
|
+
DatasetTable,
|
|
17
|
+
PandaHdf5DatasetType,
|
|
12
18
|
SeqTable,
|
|
13
19
|
SeqTableRow,
|
|
14
20
|
SeqTrigger,
|
|
15
21
|
seq_table_from_arrays,
|
|
16
22
|
seq_table_from_rows,
|
|
17
23
|
)
|
|
18
|
-
from ._trigger import
|
|
24
|
+
from ._trigger import (
|
|
25
|
+
PcompInfo,
|
|
26
|
+
SeqTableInfo,
|
|
27
|
+
StaticPcompTriggerLogic,
|
|
28
|
+
StaticSeqTableTriggerLogic,
|
|
29
|
+
)
|
|
19
30
|
from ._utils import phase_sorter
|
|
20
31
|
|
|
21
32
|
__all__ = [
|
|
22
33
|
"CommonPandaBlocks",
|
|
23
|
-
"
|
|
34
|
+
"DataBlock",
|
|
35
|
+
"EnableDisableOptions",
|
|
24
36
|
"PcapBlock",
|
|
37
|
+
"PcompBlock",
|
|
38
|
+
"PcompDirectionOptions",
|
|
25
39
|
"PulseBlock",
|
|
26
|
-
"seq_table_from_arrays",
|
|
27
|
-
"seq_table_from_rows",
|
|
28
40
|
"SeqBlock",
|
|
41
|
+
"TimeUnits",
|
|
42
|
+
"HDFPanda",
|
|
43
|
+
"PandaHDFWriter",
|
|
44
|
+
"PandaPcapController",
|
|
45
|
+
"DatasetTable",
|
|
46
|
+
"PandaHdf5DatasetType",
|
|
29
47
|
"SeqTable",
|
|
30
48
|
"SeqTableRow",
|
|
31
49
|
"SeqTrigger",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
50
|
+
"seq_table_from_arrays",
|
|
51
|
+
"seq_table_from_rows",
|
|
52
|
+
"PcompInfo",
|
|
53
|
+
"SeqTableInfo",
|
|
54
|
+
"StaticPcompTriggerLogic",
|
|
37
55
|
"StaticSeqTableTriggerLogic",
|
|
56
|
+
"phase_sorter",
|
|
38
57
|
]
|