ophyd-async 0.9.0a1__py3-none-any.whl → 0.10.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ophyd_async/__init__.py +5 -8
- ophyd_async/_docs_parser.py +12 -0
- ophyd_async/_version.py +9 -4
- ophyd_async/core/__init__.py +102 -74
- ophyd_async/core/_derived_signal.py +271 -0
- ophyd_async/core/_derived_signal_backend.py +300 -0
- ophyd_async/core/_detector.py +158 -153
- ophyd_async/core/_device.py +143 -115
- ophyd_async/core/_device_filler.py +82 -9
- ophyd_async/core/_flyer.py +16 -7
- ophyd_async/core/_hdf_dataset.py +29 -22
- ophyd_async/core/_log.py +14 -23
- ophyd_async/core/_mock_signal_backend.py +11 -3
- ophyd_async/core/_protocol.py +65 -45
- ophyd_async/core/_providers.py +28 -9
- ophyd_async/core/_readable.py +74 -58
- ophyd_async/core/_settings.py +113 -0
- ophyd_async/core/_signal.py +304 -174
- ophyd_async/core/_signal_backend.py +60 -14
- ophyd_async/core/_soft_signal_backend.py +18 -12
- ophyd_async/core/_status.py +72 -24
- ophyd_async/core/_table.py +54 -17
- ophyd_async/core/_utils.py +101 -52
- ophyd_async/core/_yaml_settings.py +66 -0
- ophyd_async/epics/__init__.py +1 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +51 -0
- ophyd_async/epics/adandor/_andor_io.py +34 -0
- ophyd_async/epics/adaravis/__init__.py +8 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -41
- ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
- ophyd_async/epics/adaravis/_aravis_io.py +13 -28
- ophyd_async/epics/adcore/__init__.py +36 -14
- ophyd_async/epics/adcore/_core_detector.py +81 -0
- ophyd_async/epics/adcore/_core_io.py +145 -95
- ophyd_async/epics/adcore/_core_logic.py +179 -88
- ophyd_async/epics/adcore/_core_writer.py +223 -0
- ophyd_async/epics/adcore/_hdf_writer.py +51 -92
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +6 -5
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +3 -2
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +32 -27
- ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
- ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
- ophyd_async/epics/adpilatus/__init__.py +7 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
- ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
- ophyd_async/epics/adsimdetector/__init__.py +8 -1
- ophyd_async/epics/adsimdetector/_sim.py +22 -16
- ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
- ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
- ophyd_async/epics/advimba/__init__.py +10 -1
- ophyd_async/epics/advimba/_vimba.py +26 -25
- ophyd_async/epics/advimba/_vimba_controller.py +12 -24
- ophyd_async/epics/advimba/_vimba_io.py +23 -28
- ophyd_async/epics/core/_aioca.py +66 -30
- ophyd_async/epics/core/_epics_connector.py +4 -0
- ophyd_async/epics/core/_epics_device.py +2 -0
- ophyd_async/epics/core/_p4p.py +50 -18
- ophyd_async/epics/core/_pvi_connector.py +65 -8
- ophyd_async/epics/core/_signal.py +51 -51
- ophyd_async/epics/core/_util.py +5 -5
- ophyd_async/epics/demo/__init__.py +11 -49
- ophyd_async/epics/demo/__main__.py +31 -0
- ophyd_async/epics/demo/_ioc.py +32 -0
- ophyd_async/epics/demo/_motor.py +82 -0
- ophyd_async/epics/demo/_point_detector.py +42 -0
- ophyd_async/epics/demo/_point_detector_channel.py +22 -0
- ophyd_async/epics/demo/_stage.py +15 -0
- ophyd_async/epics/demo/{mover.db → motor.db} +2 -1
- ophyd_async/epics/demo/point_detector.db +59 -0
- ophyd_async/epics/demo/point_detector_channel.db +21 -0
- ophyd_async/epics/eiger/_eiger.py +1 -3
- ophyd_async/epics/eiger/_eiger_controller.py +11 -4
- ophyd_async/epics/eiger/_eiger_io.py +2 -0
- ophyd_async/epics/eiger/_odin_io.py +1 -2
- ophyd_async/epics/motor.py +83 -38
- ophyd_async/epics/signal.py +4 -1
- ophyd_async/epics/testing/__init__.py +14 -14
- ophyd_async/epics/testing/_example_ioc.py +68 -73
- ophyd_async/epics/testing/_utils.py +19 -44
- ophyd_async/epics/testing/test_records.db +16 -0
- ophyd_async/epics/testing/test_records_pva.db +17 -16
- ophyd_async/fastcs/__init__.py +1 -0
- ophyd_async/fastcs/core.py +6 -0
- ophyd_async/fastcs/odin/__init__.py +1 -0
- ophyd_async/fastcs/panda/__init__.py +8 -8
- ophyd_async/fastcs/panda/_block.py +29 -9
- ophyd_async/fastcs/panda/_control.py +12 -2
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
- ophyd_async/fastcs/panda/_table.py +13 -7
- ophyd_async/fastcs/panda/_trigger.py +23 -9
- ophyd_async/fastcs/panda/_writer.py +27 -30
- ophyd_async/plan_stubs/__init__.py +16 -0
- ophyd_async/plan_stubs/_ensure_connected.py +12 -17
- ophyd_async/plan_stubs/_fly.py +3 -5
- ophyd_async/plan_stubs/_nd_attributes.py +9 -5
- ophyd_async/plan_stubs/_panda.py +14 -0
- ophyd_async/plan_stubs/_settings.py +152 -0
- ophyd_async/plan_stubs/_utils.py +3 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +29 -0
- ophyd_async/sim/__main__.py +43 -0
- ophyd_async/sim/_blob_detector.py +33 -0
- ophyd_async/sim/_blob_detector_controller.py +48 -0
- ophyd_async/sim/_blob_detector_writer.py +105 -0
- ophyd_async/sim/_mirror_horizontal.py +46 -0
- ophyd_async/sim/_mirror_vertical.py +74 -0
- ophyd_async/sim/_motor.py +233 -0
- ophyd_async/sim/_pattern_generator.py +124 -0
- ophyd_async/sim/_point_detector.py +86 -0
- ophyd_async/sim/_stage.py +19 -0
- ophyd_async/tango/__init__.py +1 -0
- ophyd_async/tango/core/__init__.py +6 -1
- ophyd_async/tango/core/_base_device.py +41 -33
- ophyd_async/tango/core/_converters.py +81 -0
- ophyd_async/tango/core/_signal.py +21 -33
- ophyd_async/tango/core/_tango_readable.py +2 -19
- ophyd_async/tango/core/_tango_transport.py +148 -74
- ophyd_async/tango/core/_utils.py +47 -0
- ophyd_async/tango/demo/_counter.py +2 -0
- ophyd_async/tango/demo/_detector.py +2 -0
- ophyd_async/tango/demo/_mover.py +10 -6
- ophyd_async/tango/demo/_tango/_servers.py +4 -0
- ophyd_async/tango/testing/__init__.py +6 -0
- ophyd_async/tango/testing/_one_of_everything.py +200 -0
- ophyd_async/testing/__init__.py +48 -7
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +200 -96
- ophyd_async/testing/_mock_signal_utils.py +59 -73
- ophyd_async/testing/_one_of_everything.py +146 -0
- ophyd_async/testing/_single_derived.py +87 -0
- ophyd_async/testing/_utils.py +3 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
- ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/demo/_mover.py +0 -95
- ophyd_async/epics/demo/_sensor.py +0 -37
- ophyd_async/epics/demo/sensor.db +0 -19
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
- ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
- ophyd_async/sim/demo/_sim_motor.py +0 -107
- ophyd_async/sim/testing/__init__.py +0 -0
- ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
- ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
ophyd_async/epics/motor.py
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
"""Support for EPICS motor record.
|
|
2
|
+
|
|
3
|
+
https://github.com/epics-modules/motor
|
|
4
|
+
"""
|
|
5
|
+
|
|
1
6
|
import asyncio
|
|
2
7
|
|
|
3
8
|
from bluesky.protocols import (
|
|
@@ -5,7 +10,9 @@ from bluesky.protocols import (
|
|
|
5
10
|
Locatable,
|
|
6
11
|
Location,
|
|
7
12
|
Preparable,
|
|
13
|
+
Reading,
|
|
8
14
|
Stoppable,
|
|
15
|
+
Subscribable,
|
|
9
16
|
)
|
|
10
17
|
from pydantic import BaseModel, Field
|
|
11
18
|
|
|
@@ -14,7 +21,9 @@ from ophyd_async.core import (
|
|
|
14
21
|
DEFAULT_TIMEOUT,
|
|
15
22
|
AsyncStatus,
|
|
16
23
|
CalculatableTimeout,
|
|
24
|
+
Callback,
|
|
17
25
|
StandardReadable,
|
|
26
|
+
StrictEnum,
|
|
18
27
|
WatchableAsyncStatus,
|
|
19
28
|
WatcherUpdate,
|
|
20
29
|
observe_value,
|
|
@@ -22,47 +31,61 @@ from ophyd_async.core import (
|
|
|
22
31
|
from ophyd_async.core import StandardReadableFormat as Format
|
|
23
32
|
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w
|
|
24
33
|
|
|
34
|
+
__all__ = ["MotorLimitsException", "FlyMotorInfo", "Motor"]
|
|
25
35
|
|
|
26
|
-
class MotorLimitsException(Exception):
|
|
27
|
-
pass
|
|
28
36
|
|
|
37
|
+
class MotorLimitsException(Exception):
|
|
38
|
+
"""Exception for invalid motor limits."""
|
|
29
39
|
|
|
30
|
-
class InvalidFlyMotorException(Exception):
|
|
31
40
|
pass
|
|
32
41
|
|
|
33
42
|
|
|
34
|
-
DEFAULT_MOTOR_FLY_TIMEOUT = 60
|
|
35
|
-
DEFAULT_WATCHER_UPDATE_FREQUENCY = 0.2
|
|
36
|
-
|
|
37
|
-
|
|
38
43
|
class FlyMotorInfo(BaseModel):
|
|
39
|
-
"""Minimal set of information required to fly a motor
|
|
44
|
+
"""Minimal set of information required to fly a motor."""
|
|
40
45
|
|
|
41
|
-
#: Absolute position of the motor once it finishes accelerating to desired
|
|
42
|
-
#: velocity, in motor EGUs
|
|
43
46
|
start_position: float = Field(frozen=True)
|
|
47
|
+
"""Absolute position of the motor once it finishes accelerating to desired
|
|
48
|
+
velocity, in motor EGUs"""
|
|
44
49
|
|
|
45
|
-
#: Absolute position of the motor once it begins decelerating from desired
|
|
46
|
-
#: velocity, in EGUs
|
|
47
50
|
end_position: float = Field(frozen=True)
|
|
51
|
+
"""Absolute position of the motor once it begins decelerating from desired
|
|
52
|
+
velocity, in EGUs"""
|
|
48
53
|
|
|
49
|
-
#: Time taken for the motor to get from start_position to end_position, excluding
|
|
50
|
-
#: run-up and run-down, in seconds.
|
|
51
54
|
time_for_move: float = Field(frozen=True, gt=0)
|
|
55
|
+
"""Time taken for the motor to get from start_position to end_position, excluding
|
|
56
|
+
run-up and run-down, in seconds."""
|
|
52
57
|
|
|
53
|
-
#: Maximum time for the complete motor move, including run up and run down.
|
|
54
|
-
#: Defaults to `time_for_move` + run up and run down times + 10s.
|
|
55
58
|
timeout: CalculatableTimeout = Field(frozen=True, default=CALCULATE_TIMEOUT)
|
|
59
|
+
"""Maximum time for the complete motor move, including run up and run down.
|
|
60
|
+
Defaults to `time_for_move` + run up and run down times + 10s."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OffsetMode(StrictEnum):
|
|
64
|
+
VARIABLE = "Variable"
|
|
65
|
+
FROZEN = "Frozen"
|
|
56
66
|
|
|
57
67
|
|
|
58
|
-
class
|
|
59
|
-
|
|
68
|
+
class UseSetMode(StrictEnum):
|
|
69
|
+
USE = "Use"
|
|
70
|
+
SET = "Set"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Motor(
|
|
74
|
+
StandardReadable,
|
|
75
|
+
Locatable[float],
|
|
76
|
+
Stoppable,
|
|
77
|
+
Flyable,
|
|
78
|
+
Preparable,
|
|
79
|
+
Subscribable[float],
|
|
80
|
+
):
|
|
81
|
+
"""Device that moves a motor record."""
|
|
60
82
|
|
|
61
83
|
def __init__(self, prefix: str, name="") -> None:
|
|
62
84
|
# Define some signals
|
|
63
85
|
with self.add_children_as_readables(Format.CONFIG_SIGNAL):
|
|
64
86
|
self.motor_egu = epics_signal_r(str, prefix + ".EGU")
|
|
65
87
|
self.velocity = epics_signal_rw(float, prefix + ".VELO")
|
|
88
|
+
self.offset = epics_signal_rw(float, prefix + ".OFF")
|
|
66
89
|
|
|
67
90
|
with self.add_children_as_readables(Format.HINTED_SIGNAL):
|
|
68
91
|
self.user_readback = epics_signal_r(float, prefix + ".RBV")
|
|
@@ -75,11 +98,16 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
75
98
|
self.motor_done_move = epics_signal_r(int, prefix + ".DMOV")
|
|
76
99
|
self.low_limit_travel = epics_signal_rw(float, prefix + ".LLM")
|
|
77
100
|
self.high_limit_travel = epics_signal_rw(float, prefix + ".HLM")
|
|
101
|
+
self.offset_freeze_switch = epics_signal_rw(OffsetMode, prefix + ".FOFF")
|
|
102
|
+
self.high_limit_switch = epics_signal_r(int, prefix + ".HLS")
|
|
103
|
+
self.low_limit_switch = epics_signal_r(int, prefix + ".LLS")
|
|
104
|
+
self.set_use_switch = epics_signal_rw(UseSetMode, prefix + ".SET")
|
|
78
105
|
|
|
79
106
|
# Note:cannot use epics_signal_x here, as the motor record specifies that
|
|
80
107
|
# we must write 1 to stop the motor. Simply processing the record is not
|
|
81
108
|
# sufficient.
|
|
82
109
|
self.motor_stop = epics_signal_w(int, prefix + ".STOP")
|
|
110
|
+
|
|
83
111
|
# Whether set() should complete successfully or not
|
|
84
112
|
self._set_success = True
|
|
85
113
|
|
|
@@ -95,15 +123,14 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
95
123
|
super().__init__(name=name)
|
|
96
124
|
|
|
97
125
|
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
126
|
+
"""Set name of the motor and its children."""
|
|
98
127
|
super().set_name(name, child_name_separator=child_name_separator)
|
|
99
128
|
# Readback should be named the same as its parent in read()
|
|
100
129
|
self.user_readback.set_name(name)
|
|
101
130
|
|
|
102
131
|
@AsyncStatus.wrap
|
|
103
132
|
async def prepare(self, value: FlyMotorInfo):
|
|
104
|
-
"""
|
|
105
|
-
breached, move to start position minus run-up distance"""
|
|
106
|
-
|
|
133
|
+
"""Move to the beginning of a suitable run-up distance ready for a flyscan."""
|
|
107
134
|
self._fly_timeout = value.timeout
|
|
108
135
|
|
|
109
136
|
# Velocity, at which motor travels from start_position to end_position, in motor
|
|
@@ -125,9 +152,9 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
125
152
|
@AsyncStatus.wrap
|
|
126
153
|
async def kickoff(self):
|
|
127
154
|
"""Begin moving motor from prepared position to final position."""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
155
|
+
if not self._fly_completed_position:
|
|
156
|
+
msg = "Motor must be prepared before attempting to kickoff"
|
|
157
|
+
raise RuntimeError(msg)
|
|
131
158
|
|
|
132
159
|
self._fly_status = self.set(
|
|
133
160
|
self._fly_completed_position, timeout=self._fly_timeout
|
|
@@ -135,12 +162,16 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
135
162
|
|
|
136
163
|
def complete(self) -> WatchableAsyncStatus:
|
|
137
164
|
"""Mark as complete once motor reaches completed position."""
|
|
138
|
-
|
|
165
|
+
if not self._fly_status:
|
|
166
|
+
msg = "kickoff not called"
|
|
167
|
+
raise RuntimeError(msg)
|
|
139
168
|
return self._fly_status
|
|
140
169
|
|
|
141
170
|
@WatchableAsyncStatus.wrap
|
|
142
|
-
async def set(
|
|
143
|
-
new_position =
|
|
171
|
+
async def set( # type: ignore
|
|
172
|
+
self, new_position: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
|
|
173
|
+
):
|
|
174
|
+
"""Move motor to the given value."""
|
|
144
175
|
self._set_success = True
|
|
145
176
|
(
|
|
146
177
|
old_position,
|
|
@@ -155,13 +186,18 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
155
186
|
self.velocity.get_value(),
|
|
156
187
|
self.acceleration_time.get_value(),
|
|
157
188
|
)
|
|
189
|
+
|
|
158
190
|
if timeout is CALCULATE_TIMEOUT:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
191
|
+
try:
|
|
192
|
+
timeout = (
|
|
193
|
+
abs((new_position - old_position) / velocity)
|
|
194
|
+
+ 2 * acceleration_time
|
|
195
|
+
+ DEFAULT_TIMEOUT
|
|
196
|
+
)
|
|
197
|
+
except ZeroDivisionError as error:
|
|
198
|
+
msg = "Mover has zero velocity"
|
|
199
|
+
raise ValueError(msg) from error
|
|
200
|
+
|
|
165
201
|
move_status = self.user_setpoint.set(new_position, wait=True, timeout=timeout)
|
|
166
202
|
async for current_position in observe_value(
|
|
167
203
|
self.user_readback, done_status=move_status
|
|
@@ -178,6 +214,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
178
214
|
raise RuntimeError("Motor was stopped")
|
|
179
215
|
|
|
180
216
|
async def stop(self, success=False):
|
|
217
|
+
"""Request to stop moving and return immediately."""
|
|
181
218
|
self._set_success = success
|
|
182
219
|
# Put with completion will never complete as we are waiting for completion on
|
|
183
220
|
# the move above, so need to pass wait=False
|
|
@@ -200,11 +237,19 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
200
237
|
return fly_velocity
|
|
201
238
|
|
|
202
239
|
async def locate(self) -> Location[float]:
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return
|
|
240
|
+
"""Return the current setpoint and readback of the motor."""
|
|
241
|
+
setpoint, readback = await asyncio.gather(
|
|
242
|
+
self.user_setpoint.get_value(), self.user_readback.get_value()
|
|
243
|
+
)
|
|
244
|
+
return Location(setpoint=setpoint, readback=readback)
|
|
245
|
+
|
|
246
|
+
def subscribe(self, function: Callback[dict[str, Reading[float]]]) -> None:
|
|
247
|
+
"""Subscribe."""
|
|
248
|
+
self.user_readback.subscribe(function)
|
|
249
|
+
|
|
250
|
+
def clear_sub(self, function: Callback[dict[str, Reading[float]]]) -> None:
|
|
251
|
+
"""Unsubscribe."""
|
|
252
|
+
self.user_readback.clear_sub(function)
|
|
208
253
|
|
|
209
254
|
async def _prepare_motor_path(
|
|
210
255
|
self, fly_velocity: float, start_position: float, end_position: float
|
ophyd_async/epics/signal.py
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
from ._example_ioc import (
|
|
2
2
|
CA_PVA_RECORDS,
|
|
3
3
|
PVA_RECORDS,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
EpicsTestCaDevice,
|
|
5
|
+
EpicsTestEnum,
|
|
6
|
+
EpicsTestIocAndDevices,
|
|
7
|
+
EpicsTestPvaDevice,
|
|
8
|
+
EpicsTestSubsetEnum,
|
|
9
|
+
EpicsTestTable,
|
|
10
10
|
)
|
|
11
|
-
from ._utils import TestingIOC,
|
|
11
|
+
from ._utils import TestingIOC, generate_random_pv_prefix
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
14
14
|
"CA_PVA_RECORDS",
|
|
15
15
|
"PVA_RECORDS",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
16
|
+
"EpicsTestCaDevice",
|
|
17
|
+
"EpicsTestEnum",
|
|
18
|
+
"EpicsTestSubsetEnum",
|
|
19
|
+
"EpicsTestPvaDevice",
|
|
20
|
+
"EpicsTestTable",
|
|
21
|
+
"EpicsTestIocAndDevices",
|
|
22
22
|
"TestingIOC",
|
|
23
|
-
"
|
|
23
|
+
"generate_random_pv_prefix",
|
|
24
24
|
]
|
|
@@ -1,50 +1,57 @@
|
|
|
1
1
|
from collections.abc import Sequence
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from typing import Annotated as A
|
|
4
|
-
from typing import Literal
|
|
5
4
|
|
|
6
5
|
import numpy as np
|
|
7
6
|
|
|
8
|
-
from ophyd_async.core import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
StrictEnum,
|
|
12
|
-
Table,
|
|
13
|
-
)
|
|
14
|
-
from ophyd_async.epics.core import (
|
|
15
|
-
EpicsDevice,
|
|
16
|
-
PvSuffix,
|
|
17
|
-
)
|
|
7
|
+
from ophyd_async.core import Array1D, SignalR, SignalRW, StrictEnum, Table
|
|
8
|
+
from ophyd_async.core._utils import SubsetEnum
|
|
9
|
+
from ophyd_async.epics.core import EpicsDevice, PvSuffix
|
|
18
10
|
|
|
19
|
-
from ._utils import TestingIOC
|
|
11
|
+
from ._utils import TestingIOC, generate_random_pv_prefix
|
|
20
12
|
|
|
21
|
-
CA_PVA_RECORDS =
|
|
22
|
-
PVA_RECORDS =
|
|
13
|
+
CA_PVA_RECORDS = Path(__file__).parent / "test_records.db"
|
|
14
|
+
PVA_RECORDS = Path(__file__).parent / "test_records_pva.db"
|
|
23
15
|
|
|
24
16
|
|
|
25
|
-
class
|
|
17
|
+
class EpicsTestEnum(StrictEnum):
|
|
18
|
+
"""For testing strict enum values in test IOCs."""
|
|
19
|
+
|
|
26
20
|
A = "Aaa"
|
|
27
21
|
B = "Bbb"
|
|
28
22
|
C = "Ccc"
|
|
29
23
|
|
|
30
24
|
|
|
31
|
-
class
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
class EpicsTestSubsetEnum(SubsetEnum):
|
|
26
|
+
"""For testing subset enum values in test IOCs."""
|
|
27
|
+
|
|
28
|
+
A = "Aaa"
|
|
29
|
+
B = "Bbb"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EpicsTestTable(Table):
|
|
33
|
+
a_bool: Array1D[np.bool_]
|
|
34
|
+
a_int: Array1D[np.int32]
|
|
35
|
+
a_float: Array1D[np.float64]
|
|
36
|
+
a_str: Sequence[str]
|
|
37
|
+
a_enum: Sequence[EpicsTestEnum]
|
|
37
38
|
|
|
38
39
|
|
|
39
|
-
class
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
class EpicsTestCaDevice(EpicsDevice):
|
|
41
|
+
"""Device for use in a channel access test IOC."""
|
|
42
|
+
|
|
43
|
+
a_int: A[SignalRW[int], PvSuffix("int")]
|
|
44
|
+
"""A thing"""
|
|
45
|
+
a_float: A[SignalRW[float], PvSuffix("float")]
|
|
46
|
+
float_prec_0: A[SignalRW[int], PvSuffix("float_prec_0")]
|
|
47
|
+
a_str: A[SignalRW[str], PvSuffix("str")]
|
|
43
48
|
longstr: A[SignalRW[str], PvSuffix("longstr")]
|
|
44
|
-
longstr2: A[SignalRW[str], PvSuffix("longstr2")]
|
|
45
|
-
|
|
46
|
-
enum: A[SignalRW[
|
|
47
|
-
enum2: A[SignalRW[
|
|
49
|
+
longstr2: A[SignalRW[str], PvSuffix("longstr2.VAL$")]
|
|
50
|
+
a_bool: A[SignalRW[bool], PvSuffix("bool")]
|
|
51
|
+
enum: A[SignalRW[EpicsTestEnum], PvSuffix("enum")]
|
|
52
|
+
enum2: A[SignalRW[EpicsTestEnum], PvSuffix("enum2")]
|
|
53
|
+
subset_enum: A[SignalRW[EpicsTestSubsetEnum], PvSuffix("subset_enum")]
|
|
54
|
+
enum_str_fallback: A[SignalRW[str], PvSuffix("enum_str_fallback")]
|
|
48
55
|
bool_unnamed: A[SignalRW[bool], PvSuffix("bool_unnamed")]
|
|
49
56
|
partialint: A[SignalRW[int], PvSuffix("partialint")]
|
|
50
57
|
lessint: A[SignalRW[int], PvSuffix("lessint")]
|
|
@@ -56,52 +63,40 @@ class ExampleCaDevice(EpicsDevice):
|
|
|
56
63
|
stra: A[SignalRW[Sequence[str]], PvSuffix("stra")]
|
|
57
64
|
|
|
58
65
|
|
|
59
|
-
class
|
|
66
|
+
class EpicsTestPvaDevice(EpicsTestCaDevice):
|
|
67
|
+
"""Device for use in a pv access test IOC."""
|
|
68
|
+
|
|
69
|
+
# pva can support all signal types that ca can
|
|
60
70
|
int8a: A[SignalRW[Array1D[np.int8]], PvSuffix("int8a")]
|
|
61
71
|
uint16a: A[SignalRW[Array1D[np.uint16]], PvSuffix("uint16a")]
|
|
62
72
|
uint32a: A[SignalRW[Array1D[np.uint32]], PvSuffix("uint32a")]
|
|
63
73
|
int64a: A[SignalRW[Array1D[np.int64]], PvSuffix("int64a")]
|
|
64
74
|
uint64a: A[SignalRW[Array1D[np.uint64]], PvSuffix("uint64a")]
|
|
65
|
-
table: A[SignalRW[
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def get_example_ioc() -> TestingIOC:
|
|
95
|
-
"""Get TestingIOC instance with the example databases loaded.
|
|
96
|
-
|
|
97
|
-
Returns
|
|
98
|
-
-------
|
|
99
|
-
TestingIOC
|
|
100
|
-
instance with test_records.db loaded for ExampleCaDevice and
|
|
101
|
-
test_records.db and test_records_pva.db loaded for ExamplePvaDevice.
|
|
102
|
-
"""
|
|
103
|
-
ioc = TestingIOC()
|
|
104
|
-
ioc.database_for(PVA_RECORDS, ExamplePvaDevice)
|
|
105
|
-
ioc.database_for(CA_PVA_RECORDS, ExamplePvaDevice)
|
|
106
|
-
ioc.database_for(CA_PVA_RECORDS, ExampleCaDevice)
|
|
107
|
-
return ioc
|
|
75
|
+
table: A[SignalRW[EpicsTestTable], PvSuffix("table")]
|
|
76
|
+
ntndarray: A[SignalR[np.ndarray], PvSuffix("ntndarray")]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class EpicsTestIocAndDevices:
|
|
80
|
+
"""Test IOC with ca and pva devices."""
|
|
81
|
+
|
|
82
|
+
def __init__(self):
|
|
83
|
+
self.prefix = generate_random_pv_prefix()
|
|
84
|
+
self.ioc = TestingIOC()
|
|
85
|
+
# Create supporting records and ExampleCaDevice
|
|
86
|
+
ca_prefix = f"{self.prefix}ca:"
|
|
87
|
+
self.ioc.add_database(CA_PVA_RECORDS, device=ca_prefix)
|
|
88
|
+
self.ca_device = EpicsTestCaDevice(f"ca://{ca_prefix}")
|
|
89
|
+
# Create supporting records and ExamplePvaDevice
|
|
90
|
+
pva_prefix = f"{self.prefix}pva:"
|
|
91
|
+
self.ioc.add_database(CA_PVA_RECORDS, device=pva_prefix)
|
|
92
|
+
self.ioc.add_database(PVA_RECORDS, device=pva_prefix)
|
|
93
|
+
self.pva_device = EpicsTestPvaDevice(f"pva://{pva_prefix}")
|
|
94
|
+
|
|
95
|
+
def get_device(self, protocol: str) -> EpicsTestCaDevice | EpicsTestPvaDevice:
|
|
96
|
+
return getattr(self, f"{protocol}_device")
|
|
97
|
+
|
|
98
|
+
def get_signal(self, protocol: str, name: str) -> SignalRW:
|
|
99
|
+
return getattr(self.get_device(protocol), name)
|
|
100
|
+
|
|
101
|
+
def get_pv(self, protocol: str, name: str) -> str:
|
|
102
|
+
return f"{protocol}://{self.prefix}{protocol}:{name}"
|
|
@@ -5,50 +5,31 @@ import sys
|
|
|
5
5
|
import time
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from aioca import purge_channel_caches
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def generate_random_PV_prefix() -> str:
|
|
9
|
+
def generate_random_pv_prefix() -> str:
|
|
10
|
+
"""For generating random PV names in test devices."""
|
|
14
11
|
return "".join(random.choice(string.ascii_lowercase) for _ in range(12)) + ":"
|
|
15
12
|
|
|
16
13
|
|
|
17
14
|
class TestingIOC:
|
|
18
|
-
|
|
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
|
|
15
|
+
"""For initialising an IOC in tests."""
|
|
28
16
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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)
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self._db_macros: list[tuple[Path, dict[str, str]]] = []
|
|
19
|
+
self.output = ""
|
|
37
20
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
return self._prefixes.setdefault(device_cls, generate_random_PV_prefix())
|
|
21
|
+
def add_database(self, db: Path | str, /, **macros: str):
|
|
22
|
+
self._db_macros.append((Path(db), macros))
|
|
41
23
|
|
|
42
|
-
def
|
|
24
|
+
def start(self):
|
|
43
25
|
ioc_args = [
|
|
44
26
|
sys.executable,
|
|
45
27
|
"-m",
|
|
46
28
|
"epicscorelibs.ioc",
|
|
47
29
|
]
|
|
48
|
-
for
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
ioc_args += ["-m", f"device={prefix}", "-d", str(db)]
|
|
30
|
+
for db, macros in self._db_macros:
|
|
31
|
+
macro_str = ",".join(f"{k}={v}" for k, v in macros.items())
|
|
32
|
+
ioc_args += ["-m", macro_str, "-d", str(db)]
|
|
52
33
|
self._process = subprocess.Popen(
|
|
53
34
|
ioc_args,
|
|
54
35
|
stdin=subprocess.PIPE,
|
|
@@ -56,23 +37,17 @@ class TestingIOC:
|
|
|
56
37
|
stderr=subprocess.STDOUT,
|
|
57
38
|
universal_newlines=True,
|
|
58
39
|
)
|
|
40
|
+
assert self._process.stdout # noqa: S101 # this is to make Pylance happy
|
|
59
41
|
start_time = time.monotonic()
|
|
60
|
-
while "iocRun: All initialization complete" not in
|
|
61
|
-
self._process.stdout.readline().strip() # type: ignore
|
|
62
|
-
):
|
|
42
|
+
while "iocRun: All initialization complete" not in self.output:
|
|
63
43
|
if time.monotonic() - start_time > 10:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Someone else already called communicate
|
|
68
|
-
pass
|
|
69
|
-
raise TimeoutError("IOC did not start in time")
|
|
44
|
+
self.stop()
|
|
45
|
+
raise TimeoutError(f"IOC did not start in time:\n{self.output}")
|
|
46
|
+
self.output += self._process.stdout.readline()
|
|
70
47
|
|
|
71
|
-
def
|
|
72
|
-
# close backend caches before the event loop
|
|
73
|
-
purge_channel_caches()
|
|
48
|
+
def stop(self):
|
|
74
49
|
try:
|
|
75
|
-
|
|
50
|
+
self.output += self._process.communicate("exit()")[0]
|
|
76
51
|
except ValueError:
|
|
77
52
|
# Someone else already called communicate
|
|
78
53
|
pass
|
|
@@ -96,6 +96,22 @@ record(mbbo, "$(device)enum2") {
|
|
|
96
96
|
field(PINI, "YES")
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
record(mbbo, "$(device)subset_enum") {
|
|
100
|
+
field(ZRST, "Aaa")
|
|
101
|
+
field(ONST, "Bbb")
|
|
102
|
+
field(TWST, "Ccc")
|
|
103
|
+
field(VAL, "1")
|
|
104
|
+
field(PINI, "YES")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
record(mbbo, "$(device)enum_str_fallback") {
|
|
108
|
+
field(ZRST, "Aaa")
|
|
109
|
+
field(ONST, "Bbb")
|
|
110
|
+
field(TWST, "Ccc")
|
|
111
|
+
field(VAL, "1")
|
|
112
|
+
field(PINI, "YES")
|
|
113
|
+
}
|
|
114
|
+
|
|
99
115
|
record(waveform, "$(device)uint8a") {
|
|
100
116
|
field(NELM, "3")
|
|
101
117
|
field(FTVL, "UCHAR")
|
|
@@ -1,36 +1,37 @@
|
|
|
1
1
|
record(waveform, "$(device)int8a") {
|
|
2
|
-
field(NELM, "
|
|
2
|
+
field(NELM, "7")
|
|
3
3
|
field(FTVL, "CHAR")
|
|
4
|
-
field(INP, {const:[-128, 127]})
|
|
4
|
+
field(INP, {const:[-128, 127, 0, 1, 2, 3, 4]})
|
|
5
5
|
field(PINI, "YES")
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
record(waveform, "$(device)uint16a") {
|
|
9
|
-
field(NELM, "
|
|
9
|
+
field(NELM, "7")
|
|
10
10
|
field(FTVL, "USHORT")
|
|
11
|
-
field(INP, {const:[0, 65535]})
|
|
11
|
+
field(INP, {const:[0, 65535, 0, 1, 2, 3, 4]})
|
|
12
12
|
field(PINI, "YES")
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
record(waveform, "$(device)uint32a") {
|
|
16
|
-
field(NELM, "
|
|
16
|
+
field(NELM, "7")
|
|
17
17
|
field(FTVL, "ULONG")
|
|
18
|
-
field(INP, {const:[0, 4294967295]})
|
|
18
|
+
field(INP, {const:[0, 4294967295, 0, 1, 2, 3, 4]})
|
|
19
19
|
field(PINI, "YES")
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
record(waveform, "$(device)int64a") {
|
|
23
|
-
field(NELM, "
|
|
23
|
+
field(NELM, "7")
|
|
24
24
|
field(FTVL, "INT64")
|
|
25
|
-
#
|
|
26
|
-
field(INP, {const:[-
|
|
25
|
+
# limit of range appears to be +/-(2^63 - 1)
|
|
26
|
+
field(INP, {const:[-9223372036854775807, 9223372036854775807, 0, 1, 2, 3, 4]})
|
|
27
27
|
field(PINI, "YES")
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
record(waveform, "$(device)uint64a") {
|
|
31
|
-
field(NELM, "
|
|
31
|
+
field(NELM, "7")
|
|
32
32
|
field(FTVL, "UINT64")
|
|
33
|
-
|
|
33
|
+
# limit of range appears to be 0 to +(2^63 - 1)
|
|
34
|
+
field(INP, {const:[0, 9223372036854775807, 0, 1, 2, 3, 4]})
|
|
34
35
|
field(PINI, "YES")
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -58,7 +59,7 @@ record(waveform, "$(device)table:bool")
|
|
|
58
59
|
field(PINI, "YES")
|
|
59
60
|
info(Q:group, {
|
|
60
61
|
"$(device)table": {
|
|
61
|
-
"value.
|
|
62
|
+
"value.a_bool": {
|
|
62
63
|
"+type": "plain",
|
|
63
64
|
"+channel": "VAL",
|
|
64
65
|
"+putorder": 1
|
|
@@ -75,7 +76,7 @@ record(waveform, "$(device)table:int")
|
|
|
75
76
|
field(PINI, "YES")
|
|
76
77
|
info(Q:group, {
|
|
77
78
|
"$(device)table": {
|
|
78
|
-
"value.
|
|
79
|
+
"value.a_int": {
|
|
79
80
|
"+type": "plain",
|
|
80
81
|
"+channel": "VAL",
|
|
81
82
|
"+putorder": 2
|
|
@@ -92,7 +93,7 @@ record(waveform, "$(device)table:float")
|
|
|
92
93
|
field(PINI, "YES")
|
|
93
94
|
info(Q:group, {
|
|
94
95
|
"$(device)table": {
|
|
95
|
-
"value.
|
|
96
|
+
"value.a_float": {
|
|
96
97
|
"+type": "plain",
|
|
97
98
|
"+channel": "VAL",
|
|
98
99
|
"+putorder": 3
|
|
@@ -109,7 +110,7 @@ record(waveform, "$(device)table:str")
|
|
|
109
110
|
field(PINI, "YES")
|
|
110
111
|
info(Q:group, {
|
|
111
112
|
"$(device)table": {
|
|
112
|
-
"value.
|
|
113
|
+
"value.a_str": {
|
|
113
114
|
"+type": "plain",
|
|
114
115
|
"+channel": "VAL",
|
|
115
116
|
"+putorder": 4
|
|
@@ -126,7 +127,7 @@ record(waveform, "$(device)table:enum")
|
|
|
126
127
|
field(PINI, "YES")
|
|
127
128
|
info(Q:group, {
|
|
128
129
|
"$(device)table": {
|
|
129
|
-
"value.
|
|
130
|
+
"value.a_enum": {
|
|
130
131
|
"+type": "plain",
|
|
131
132
|
"+channel": "VAL",
|
|
132
133
|
"+putorder": 5,
|
ophyd_async/fastcs/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FastCS support for Signals via EPICS or Tango, and Devices that use them."""
|