ophyd-async 0.9.0a1__py3-none-any.whl → 0.9.0a2__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 +1 -1
- ophyd_async/core/__init__.py +13 -20
- ophyd_async/core/_detector.py +61 -37
- ophyd_async/core/_device.py +102 -80
- ophyd_async/core/_device_filler.py +17 -8
- ophyd_async/core/_flyer.py +2 -2
- ophyd_async/core/_readable.py +30 -23
- ophyd_async/core/_settings.py +104 -0
- ophyd_async/core/_signal.py +55 -17
- ophyd_async/core/_signal_backend.py +4 -1
- ophyd_async/core/_soft_signal_backend.py +2 -1
- ophyd_async/core/_table.py +18 -10
- ophyd_async/core/_utils.py +5 -3
- ophyd_async/core/_yaml_settings.py +64 -0
- ophyd_async/epics/adandor/__init__.py +9 -0
- ophyd_async/epics/adandor/_andor.py +45 -0
- ophyd_async/epics/adandor/_andor_controller.py +49 -0
- ophyd_async/epics/adandor/_andor_io.py +36 -0
- ophyd_async/epics/adaravis/__init__.py +3 -1
- ophyd_async/epics/adaravis/_aravis.py +23 -37
- ophyd_async/epics/adaravis/_aravis_controller.py +13 -22
- ophyd_async/epics/adcore/__init__.py +15 -8
- ophyd_async/epics/adcore/_core_detector.py +41 -0
- ophyd_async/epics/adcore/_core_io.py +35 -10
- ophyd_async/epics/adcore/_core_logic.py +98 -86
- ophyd_async/epics/adcore/_core_writer.py +219 -0
- ophyd_async/epics/adcore/_hdf_writer.py +38 -62
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +4 -3
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix.py +29 -24
- ophyd_async/epics/adkinetix/_kinetix_controller.py +9 -21
- ophyd_async/epics/adpilatus/__init__.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus.py +27 -39
- ophyd_async/epics/adpilatus/_pilatus_controller.py +44 -22
- ophyd_async/epics/adsimdetector/__init__.py +3 -3
- ophyd_async/epics/adsimdetector/_sim.py +33 -17
- ophyd_async/epics/advimba/_vimba.py +23 -23
- ophyd_async/epics/advimba/_vimba_controller.py +10 -24
- ophyd_async/epics/core/_aioca.py +31 -14
- ophyd_async/epics/core/_p4p.py +40 -16
- ophyd_async/epics/core/_util.py +1 -1
- ophyd_async/epics/motor.py +18 -10
- ophyd_async/epics/sim/_ioc.py +29 -0
- ophyd_async/epics/{demo → sim}/_mover.py +10 -4
- ophyd_async/epics/testing/__init__.py +14 -14
- ophyd_async/epics/testing/_example_ioc.py +48 -65
- ophyd_async/epics/testing/_utils.py +17 -45
- ophyd_async/epics/testing/test_records.db +8 -0
- ophyd_async/fastcs/panda/__init__.py +0 -2
- ophyd_async/fastcs/panda/_control.py +7 -2
- ophyd_async/fastcs/panda/_hdf_panda.py +3 -1
- ophyd_async/fastcs/panda/_table.py +4 -1
- ophyd_async/plan_stubs/__init__.py +14 -0
- ophyd_async/plan_stubs/_ensure_connected.py +11 -17
- ophyd_async/plan_stubs/_fly.py +1 -1
- ophyd_async/plan_stubs/_nd_attributes.py +7 -5
- ophyd_async/plan_stubs/_panda.py +13 -0
- ophyd_async/plan_stubs/_settings.py +125 -0
- ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
- ophyd_async/sim/__init__.py +19 -0
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
- ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
- ophyd_async/tango/core/_signal.py +3 -1
- ophyd_async/tango/core/_tango_transport.py +12 -14
- ophyd_async/tango/{demo → sim}/_mover.py +5 -2
- ophyd_async/testing/__init__.py +19 -0
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +88 -40
- ophyd_async/testing/_mock_signal_utils.py +3 -3
- ophyd_async/testing/_one_of_everything.py +126 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/METADATA +2 -2
- ophyd_async-0.9.0a2.dist-info/RECORD +129 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/WHEEL +1 -1
- ophyd_async/core/_device_save_loader.py +0 -274
- ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
- ophyd_async/fastcs/panda/_utils.py +0 -16
- ophyd_async/sim/demo/__init__.py +0 -19
- 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/epics/{demo → sim}/__init__.py +0 -0
- /ophyd_async/epics/{demo → sim}/_sensor.py +0 -0
- /ophyd_async/epics/{demo → sim}/mover.db +0 -0
- /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
- /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
- /ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +0 -0
- /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_counter.py +0 -0
- /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
- /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/LICENSE +0 -0
- {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/top_level.txt +0 -0
ophyd_async/epics/motor.py
CHANGED
|
@@ -63,6 +63,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
63
63
|
with self.add_children_as_readables(Format.CONFIG_SIGNAL):
|
|
64
64
|
self.motor_egu = epics_signal_r(str, prefix + ".EGU")
|
|
65
65
|
self.velocity = epics_signal_rw(float, prefix + ".VELO")
|
|
66
|
+
self.offset = epics_signal_rw(float, prefix + ".OFF")
|
|
66
67
|
|
|
67
68
|
with self.add_children_as_readables(Format.HINTED_SIGNAL):
|
|
68
69
|
self.user_readback = epics_signal_r(float, prefix + ".RBV")
|
|
@@ -125,9 +126,9 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
125
126
|
@AsyncStatus.wrap
|
|
126
127
|
async def kickoff(self):
|
|
127
128
|
"""Begin moving motor from prepared position to final position."""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
if not self._fly_completed_position:
|
|
130
|
+
msg = "Motor must be prepared before attempting to kickoff"
|
|
131
|
+
raise RuntimeError(msg)
|
|
131
132
|
|
|
132
133
|
self._fly_status = self.set(
|
|
133
134
|
self._fly_completed_position, timeout=self._fly_timeout
|
|
@@ -135,7 +136,9 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
135
136
|
|
|
136
137
|
def complete(self) -> WatchableAsyncStatus:
|
|
137
138
|
"""Mark as complete once motor reaches completed position."""
|
|
138
|
-
|
|
139
|
+
if not self._fly_status:
|
|
140
|
+
msg = "kickoff not called"
|
|
141
|
+
raise RuntimeError(msg)
|
|
139
142
|
return self._fly_status
|
|
140
143
|
|
|
141
144
|
@WatchableAsyncStatus.wrap
|
|
@@ -155,13 +158,18 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
|
|
|
155
158
|
self.velocity.get_value(),
|
|
156
159
|
self.acceleration_time.get_value(),
|
|
157
160
|
)
|
|
161
|
+
|
|
158
162
|
if timeout is CALCULATE_TIMEOUT:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
try:
|
|
164
|
+
timeout = (
|
|
165
|
+
abs((new_position - old_position) / velocity)
|
|
166
|
+
+ 2 * acceleration_time
|
|
167
|
+
+ DEFAULT_TIMEOUT
|
|
168
|
+
)
|
|
169
|
+
except ZeroDivisionError as error:
|
|
170
|
+
msg = "Mover has zero velocity"
|
|
171
|
+
raise ValueError(msg) from error
|
|
172
|
+
|
|
165
173
|
move_status = self.user_setpoint.set(new_position, wait=True, timeout=timeout)
|
|
166
174
|
async for current_position in observe_value(
|
|
167
175
|
self.user_readback, done_status=move_status
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from ophyd_async.epics.testing import TestingIOC
|
|
5
|
+
|
|
6
|
+
HERE = Path(__file__).absolute().parent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def start_ioc_subprocess(prefix: str, num_counters: int):
|
|
10
|
+
"""Start an IOC subprocess with EPICS database for sample stage and sensor
|
|
11
|
+
with the same pv prefix
|
|
12
|
+
"""
|
|
13
|
+
ioc = TestingIOC()
|
|
14
|
+
# Create X and Y motors
|
|
15
|
+
for suffix in ["X", "Y"]:
|
|
16
|
+
ioc.add_database(HERE / "mover.db", P=f"{prefix}STAGE:{suffix}:")
|
|
17
|
+
# Create a multichannel counter with num_counters
|
|
18
|
+
ioc.add_database(HERE / "multichannelcounter.db", P=f"{prefix}MCC:")
|
|
19
|
+
for i in range(1, num_counters + 1):
|
|
20
|
+
ioc.add_database(
|
|
21
|
+
HERE / "counter.db",
|
|
22
|
+
P=f"{prefix}MCC:",
|
|
23
|
+
CHANNEL=str(i),
|
|
24
|
+
X=f"{prefix}STAGE:X:",
|
|
25
|
+
Y=f"{prefix}STAGE:Y:",
|
|
26
|
+
)
|
|
27
|
+
# Start IOC and register it to be stopped at exit
|
|
28
|
+
ioc.start()
|
|
29
|
+
atexit.register(ioc.stop)
|
|
@@ -52,13 +52,19 @@ class Mover(StandardReadable, Movable, Stoppable):
|
|
|
52
52
|
self.precision.get_value(),
|
|
53
53
|
self.velocity.get_value(),
|
|
54
54
|
)
|
|
55
|
-
if timeout
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
if timeout is CALCULATE_TIMEOUT:
|
|
56
|
+
try:
|
|
57
|
+
timeout = (
|
|
58
|
+
abs((new_position - old_position) / velocity) + DEFAULT_TIMEOUT
|
|
59
|
+
)
|
|
60
|
+
except ZeroDivisionError as error:
|
|
61
|
+
msg = "Mover has zero velocity"
|
|
62
|
+
raise ValueError(msg) from error
|
|
63
|
+
|
|
58
64
|
# Make an Event that will be set on completion, and a Status that will
|
|
59
65
|
# error if not done in time
|
|
60
66
|
done = asyncio.Event()
|
|
61
|
-
done_status = AsyncStatus(asyncio.wait_for(done.wait(), timeout))
|
|
67
|
+
done_status = AsyncStatus(asyncio.wait_for(done.wait(), timeout)) # type: ignore
|
|
62
68
|
# Wait for the value to set, but don't wait for put completion callback
|
|
63
69
|
await self.setpoint.set(new_position, wait=False)
|
|
64
70
|
async for current_position in observe_value(
|
|
@@ -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,49 @@
|
|
|
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):
|
|
26
18
|
A = "Aaa"
|
|
27
19
|
B = "Bbb"
|
|
28
20
|
C = "Ccc"
|
|
29
21
|
|
|
30
22
|
|
|
31
|
-
class
|
|
23
|
+
class EpicsTestSubsetEnum(SubsetEnum):
|
|
24
|
+
A = "Aaa"
|
|
25
|
+
B = "Bbb"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EpicsTestTable(Table):
|
|
32
29
|
bool: Array1D[np.bool_]
|
|
33
30
|
int: Array1D[np.int32]
|
|
34
31
|
float: Array1D[np.float64]
|
|
35
32
|
str: Sequence[str]
|
|
36
|
-
enum: Sequence[
|
|
33
|
+
enum: Sequence[EpicsTestEnum]
|
|
37
34
|
|
|
38
35
|
|
|
39
|
-
class
|
|
36
|
+
class EpicsTestCaDevice(EpicsDevice):
|
|
40
37
|
my_int: A[SignalRW[int], PvSuffix("int")]
|
|
41
38
|
my_float: A[SignalRW[float], PvSuffix("float")]
|
|
39
|
+
float_prec_0: A[SignalRW[int], PvSuffix("float_prec_0")]
|
|
42
40
|
my_str: A[SignalRW[str], PvSuffix("str")]
|
|
43
41
|
longstr: A[SignalRW[str], PvSuffix("longstr")]
|
|
44
|
-
longstr2: A[SignalRW[str], PvSuffix("longstr2")]
|
|
42
|
+
longstr2: A[SignalRW[str], PvSuffix("longstr2.VAL$")]
|
|
45
43
|
my_bool: A[SignalRW[bool], PvSuffix("bool")]
|
|
46
|
-
enum: A[SignalRW[
|
|
47
|
-
enum2: A[SignalRW[
|
|
44
|
+
enum: A[SignalRW[EpicsTestEnum], PvSuffix("enum")]
|
|
45
|
+
enum2: A[SignalRW[EpicsTestEnum], PvSuffix("enum2")]
|
|
46
|
+
subset_enum: A[SignalRW[EpicsTestSubsetEnum], PvSuffix("subset_enum")]
|
|
48
47
|
bool_unnamed: A[SignalRW[bool], PvSuffix("bool_unnamed")]
|
|
49
48
|
partialint: A[SignalRW[int], PvSuffix("partialint")]
|
|
50
49
|
lessint: A[SignalRW[int], PvSuffix("lessint")]
|
|
@@ -56,52 +55,36 @@ class ExampleCaDevice(EpicsDevice):
|
|
|
56
55
|
stra: A[SignalRW[Sequence[str]], PvSuffix("stra")]
|
|
57
56
|
|
|
58
57
|
|
|
59
|
-
class
|
|
58
|
+
class EpicsTestPvaDevice(EpicsTestCaDevice):
|
|
59
|
+
# pva can support all signal types that ca can
|
|
60
60
|
int8a: A[SignalRW[Array1D[np.int8]], PvSuffix("int8a")]
|
|
61
61
|
uint16a: A[SignalRW[Array1D[np.uint16]], PvSuffix("uint16a")]
|
|
62
62
|
uint32a: A[SignalRW[Array1D[np.uint32]], PvSuffix("uint32a")]
|
|
63
63
|
int64a: A[SignalRW[Array1D[np.int64]], PvSuffix("int64a")]
|
|
64
64
|
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
|
-
return device
|
|
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
|
|
65
|
+
table: A[SignalRW[EpicsTestTable], PvSuffix("table")]
|
|
66
|
+
ntndarray: A[SignalR[np.ndarray], PvSuffix("ntndarray")]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EpicsTestIocAndDevices:
|
|
70
|
+
def __init__(self):
|
|
71
|
+
self.prefix = generate_random_pv_prefix()
|
|
72
|
+
self.ioc = TestingIOC()
|
|
73
|
+
# Create supporting records and ExampleCaDevice
|
|
74
|
+
ca_prefix = f"{self.prefix}ca:"
|
|
75
|
+
self.ioc.add_database(CA_PVA_RECORDS, device=ca_prefix)
|
|
76
|
+
self.ca_device = EpicsTestCaDevice(f"ca://{ca_prefix}")
|
|
77
|
+
# Create supporting records and ExamplePvaDevice
|
|
78
|
+
pva_prefix = f"{self.prefix}pva:"
|
|
79
|
+
self.ioc.add_database(CA_PVA_RECORDS, device=pva_prefix)
|
|
80
|
+
self.ioc.add_database(PVA_RECORDS, device=pva_prefix)
|
|
81
|
+
self.pva_device = EpicsTestPvaDevice(f"pva://{pva_prefix}")
|
|
82
|
+
|
|
83
|
+
def get_device(self, protocol: str) -> EpicsTestCaDevice | EpicsTestPvaDevice:
|
|
84
|
+
return getattr(self, f"{protocol}_device")
|
|
85
|
+
|
|
86
|
+
def get_signal(self, protocol: str, name: str) -> SignalRW:
|
|
87
|
+
return getattr(self.get_device(protocol), name)
|
|
88
|
+
|
|
89
|
+
def get_pv(self, protocol: str, name: str) -> str:
|
|
90
|
+
return f"{protocol}://{self.prefix}{protocol}:{name}"
|
|
@@ -5,50 +5,28 @@ 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:
|
|
14
10
|
return "".join(random.choice(string.ascii_lowercase) for _ in range(12)) + ":"
|
|
15
11
|
|
|
16
12
|
|
|
17
13
|
class TestingIOC:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@classmethod
|
|
22
|
-
def with_database(cls, db: Path | str): # use as a decorator
|
|
23
|
-
def inner(device_cls: type[Device]):
|
|
24
|
-
cls.database_for(db, device_cls)
|
|
25
|
-
return device_cls
|
|
26
|
-
|
|
27
|
-
return inner
|
|
28
|
-
|
|
29
|
-
@classmethod
|
|
30
|
-
def database_for(cls, db, device_cls):
|
|
31
|
-
path = Path(db)
|
|
32
|
-
if not path.is_file():
|
|
33
|
-
raise OSError(f"{path} is not a file.")
|
|
34
|
-
if device_cls not in cls._dbs:
|
|
35
|
-
cls._dbs[device_cls] = []
|
|
36
|
-
cls._dbs[device_cls].append(path)
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self._db_macros: list[tuple[Path, dict[str, str]]] = []
|
|
16
|
+
self.output = ""
|
|
37
17
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
return self._prefixes.setdefault(device_cls, generate_random_PV_prefix())
|
|
18
|
+
def add_database(self, db: Path | str, /, **macros: str):
|
|
19
|
+
self._db_macros.append((Path(db), macros))
|
|
41
20
|
|
|
42
|
-
def
|
|
21
|
+
def start(self):
|
|
43
22
|
ioc_args = [
|
|
44
23
|
sys.executable,
|
|
45
24
|
"-m",
|
|
46
25
|
"epicscorelibs.ioc",
|
|
47
26
|
]
|
|
48
|
-
for
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
ioc_args += ["-m", f"device={prefix}", "-d", str(db)]
|
|
27
|
+
for db, macros in self._db_macros:
|
|
28
|
+
macro_str = ",".join(f"{k}={v}" for k, v in macros.items())
|
|
29
|
+
ioc_args += ["-m", macro_str, "-d", str(db)]
|
|
52
30
|
self._process = subprocess.Popen(
|
|
53
31
|
ioc_args,
|
|
54
32
|
stdin=subprocess.PIPE,
|
|
@@ -56,23 +34,17 @@ class TestingIOC:
|
|
|
56
34
|
stderr=subprocess.STDOUT,
|
|
57
35
|
universal_newlines=True,
|
|
58
36
|
)
|
|
37
|
+
assert self._process.stdout # noqa: S101 # this is to make Pylance happy
|
|
59
38
|
start_time = time.monotonic()
|
|
60
|
-
while "iocRun: All initialization complete" not in
|
|
61
|
-
self._process.stdout.readline().strip() # type: ignore
|
|
62
|
-
):
|
|
39
|
+
while "iocRun: All initialization complete" not in self.output:
|
|
63
40
|
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")
|
|
41
|
+
self.stop()
|
|
42
|
+
raise TimeoutError(f"IOC did not start in time:\n{self.output}")
|
|
43
|
+
self.output += self._process.stdout.readline()
|
|
70
44
|
|
|
71
|
-
def
|
|
72
|
-
# close backend caches before the event loop
|
|
73
|
-
purge_channel_caches()
|
|
45
|
+
def stop(self):
|
|
74
46
|
try:
|
|
75
|
-
|
|
47
|
+
self.output += self._process.communicate("exit()")[0]
|
|
76
48
|
except ValueError:
|
|
77
49
|
# Someone else already called communicate
|
|
78
50
|
pass
|
|
@@ -96,6 +96,14 @@ 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
|
+
|
|
99
107
|
record(waveform, "$(device)uint8a") {
|
|
100
108
|
field(NELM, "3")
|
|
101
109
|
field(FTVL, "UCHAR")
|
|
@@ -23,7 +23,6 @@ from ._trigger import (
|
|
|
23
23
|
StaticPcompTriggerLogic,
|
|
24
24
|
StaticSeqTableTriggerLogic,
|
|
25
25
|
)
|
|
26
|
-
from ._utils import phase_sorter
|
|
27
26
|
from ._writer import PandaHDFWriter
|
|
28
27
|
|
|
29
28
|
__all__ = [
|
|
@@ -47,5 +46,4 @@ __all__ = [
|
|
|
47
46
|
"SeqTableInfo",
|
|
48
47
|
"StaticPcompTriggerLogic",
|
|
49
48
|
"StaticSeqTableTriggerLogic",
|
|
50
|
-
"phase_sorter",
|
|
51
49
|
]
|
|
@@ -18,10 +18,15 @@ class PandaPcapController(DetectorController):
|
|
|
18
18
|
return 0.000000008
|
|
19
19
|
|
|
20
20
|
async def prepare(self, trigger_info: TriggerInfo):
|
|
21
|
-
|
|
21
|
+
if trigger_info.trigger not in (
|
|
22
22
|
DetectorTrigger.CONSTANT_GATE,
|
|
23
23
|
DetectorTrigger.VARIABLE_GATE,
|
|
24
|
-
)
|
|
24
|
+
):
|
|
25
|
+
msg = (
|
|
26
|
+
"Only constant_gate and variable_gate triggering is supported on "
|
|
27
|
+
"the PandA",
|
|
28
|
+
)
|
|
29
|
+
raise TypeError(msg)
|
|
25
30
|
|
|
26
31
|
async def arm(self):
|
|
27
32
|
self._arm_status = self.pcap.arm.set(True)
|
|
@@ -12,7 +12,9 @@ from ._writer import PandaHDFWriter
|
|
|
12
12
|
MINIMUM_PANDA_IOC = "0.11.4"
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class HDFPanda(
|
|
15
|
+
class HDFPanda(
|
|
16
|
+
CommonPandaBlocks, StandardDetector[PandaPcapController, PandaHDFWriter]
|
|
17
|
+
):
|
|
16
18
|
def __init__(
|
|
17
19
|
self,
|
|
18
20
|
prefix: str,
|
|
@@ -83,5 +83,8 @@ class SeqTable(Table):
|
|
|
83
83
|
"""
|
|
84
84
|
|
|
85
85
|
first_length = len(self)
|
|
86
|
-
|
|
86
|
+
max_length = 4096
|
|
87
|
+
if first_length > max_length:
|
|
88
|
+
msg = f"Length {first_length} is too long"
|
|
89
|
+
raise ValueError(msg)
|
|
87
90
|
return self
|
|
@@ -5,6 +5,14 @@ from ._fly import (
|
|
|
5
5
|
time_resolved_fly_and_collect_with_static_seq_table,
|
|
6
6
|
)
|
|
7
7
|
from ._nd_attributes import setup_ndattributes, setup_ndstats_sum
|
|
8
|
+
from ._panda import apply_panda_settings
|
|
9
|
+
from ._settings import (
|
|
10
|
+
apply_settings,
|
|
11
|
+
apply_settings_if_different,
|
|
12
|
+
get_current_settings,
|
|
13
|
+
retrieve_settings,
|
|
14
|
+
store_settings,
|
|
15
|
+
)
|
|
8
16
|
|
|
9
17
|
__all__ = [
|
|
10
18
|
"fly_and_collect",
|
|
@@ -13,4 +21,10 @@ __all__ = [
|
|
|
13
21
|
"ensure_connected",
|
|
14
22
|
"setup_ndattributes",
|
|
15
23
|
"setup_ndstats_sum",
|
|
24
|
+
"apply_panda_settings",
|
|
25
|
+
"apply_settings",
|
|
26
|
+
"apply_settings_if_different",
|
|
27
|
+
"get_current_settings",
|
|
28
|
+
"retrieve_settings",
|
|
29
|
+
"store_settings",
|
|
16
30
|
]
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
import bluesky.plan_stubs as bps
|
|
1
|
+
from bluesky.utils import plan
|
|
4
2
|
|
|
5
3
|
from ophyd_async.core import DEFAULT_TIMEOUT, Device, LazyMock, wait_for_connection
|
|
6
4
|
|
|
5
|
+
from ._wait_for_awaitable import wait_for_awaitable
|
|
6
|
+
|
|
7
7
|
|
|
8
|
+
@plan
|
|
8
9
|
def ensure_connected(
|
|
9
10
|
*devices: Device,
|
|
10
11
|
mock: bool | LazyMock = False,
|
|
@@ -17,17 +18,10 @@ def ensure_connected(
|
|
|
17
18
|
}
|
|
18
19
|
if non_unique:
|
|
19
20
|
raise ValueError(f"Devices do not have unique names {non_unique}")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
return wait_for_connection(**coros)
|
|
29
|
-
|
|
30
|
-
(connect_task,) = yield from bps.wait_for([connect_devices])
|
|
31
|
-
|
|
32
|
-
if connect_task and connect_task.exception() is not None:
|
|
33
|
-
raise connect_task.exception()
|
|
21
|
+
coros = {
|
|
22
|
+
device.name: device.connect(
|
|
23
|
+
mock=mock, timeout=timeout, force_reconnect=force_reconnect
|
|
24
|
+
)
|
|
25
|
+
for device in devices
|
|
26
|
+
}
|
|
27
|
+
yield from wait_for_awaitable(wait_for_connection(**coros))
|
ophyd_async/plan_stubs/_fly.py
CHANGED
|
@@ -58,7 +58,7 @@ def prepare_static_seq_table_flyer_and_detectors_with_same_trigger(
|
|
|
58
58
|
if not detectors:
|
|
59
59
|
raise ValueError("No detectors provided. There must be at least one.")
|
|
60
60
|
|
|
61
|
-
deadtime = max(det.
|
|
61
|
+
deadtime = max(det._controller.get_deadtime(exposure) for det in detectors) # noqa: SLF001
|
|
62
62
|
|
|
63
63
|
trigger_info = TriggerInfo(
|
|
64
64
|
number_of_triggers=number_of_frames * repeats,
|
|
@@ -50,11 +50,13 @@ def setup_ndattributes(
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def setup_ndstats_sum(detector: Device):
|
|
53
|
-
hdf = getattr(detector, "
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
hdf = getattr(detector, "fileio", None)
|
|
54
|
+
if not isinstance(hdf, NDFileHDFIO):
|
|
55
|
+
msg = (
|
|
56
|
+
f"Expected {detector.name} to have 'fileio' attribute that is an "
|
|
57
|
+
f"NDFileHDFIO, got {hdf}"
|
|
58
|
+
)
|
|
59
|
+
raise TypeError(msg)
|
|
58
60
|
yield from (
|
|
59
61
|
setup_ndattributes(
|
|
60
62
|
hdf,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from bluesky.utils import MsgGenerator, plan
|
|
2
|
+
|
|
3
|
+
from ophyd_async.core import Settings
|
|
4
|
+
from ophyd_async.fastcs import panda
|
|
5
|
+
|
|
6
|
+
from ._settings import apply_settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@plan
|
|
10
|
+
def apply_panda_settings(settings: Settings[panda.HDFPanda]) -> MsgGenerator[None]:
|
|
11
|
+
units, others = settings.partition(lambda signal: signal.name.endswith("_units"))
|
|
12
|
+
yield from apply_settings(units)
|
|
13
|
+
yield from apply_settings(others)
|