ophyd-async 0.8.0a6__py3-none-any.whl → 0.9.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 +15 -46
- ophyd_async/core/_detector.py +68 -44
- ophyd_async/core/_device.py +120 -79
- ophyd_async/core/_device_filler.py +17 -8
- ophyd_async/core/_flyer.py +2 -2
- ophyd_async/core/_protocol.py +0 -28
- ophyd_async/core/_readable.py +30 -23
- ophyd_async/core/_settings.py +104 -0
- ophyd_async/core/_signal.py +91 -151
- 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 +30 -5
- 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 +21 -30
- ophyd_async/epics/adaravis/_aravis_io.py +4 -4
- ophyd_async/epics/adcore/__init__.py +15 -8
- ophyd_async/epics/adcore/_core_detector.py +41 -0
- ophyd_async/epics/adcore/_core_io.py +56 -31
- ophyd_async/epics/adcore/_core_logic.py +99 -86
- ophyd_async/epics/adcore/_core_writer.py +219 -0
- ophyd_async/epics/adcore/_hdf_writer.py +33 -59
- ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
- ophyd_async/epics/adcore/_single_trigger.py +5 -4
- ophyd_async/epics/adcore/_tiff_writer.py +26 -0
- ophyd_async/epics/adcore/_utils.py +37 -36
- ophyd_async/epics/adkinetix/_kinetix.py +29 -24
- ophyd_async/epics/adkinetix/_kinetix_controller.py +15 -27
- ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
- ophyd_async/epics/adpilatus/__init__.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus.py +28 -40
- ophyd_async/epics/adpilatus/_pilatus_controller.py +47 -25
- ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
- 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 +21 -35
- ophyd_async/epics/advimba/_vimba_io.py +23 -23
- ophyd_async/epics/core/_aioca.py +52 -21
- ophyd_async/epics/core/_p4p.py +59 -16
- ophyd_async/epics/core/_pvi_connector.py +4 -2
- ophyd_async/epics/core/_signal.py +9 -2
- ophyd_async/epics/core/_util.py +10 -1
- ophyd_async/epics/eiger/_eiger_controller.py +4 -4
- ophyd_async/epics/eiger/_eiger_io.py +3 -3
- ophyd_async/epics/motor.py +26 -15
- ophyd_async/epics/sim/_ioc.py +29 -0
- ophyd_async/epics/{demo → sim}/_mover.py +12 -6
- ophyd_async/epics/{demo → sim}/_sensor.py +2 -2
- ophyd_async/epics/testing/__init__.py +14 -14
- ophyd_async/epics/testing/_example_ioc.py +53 -67
- ophyd_async/epics/testing/_utils.py +17 -45
- ophyd_async/epics/testing/test_records.db +22 -0
- ophyd_async/fastcs/core.py +2 -2
- ophyd_async/fastcs/panda/__init__.py +0 -2
- ophyd_async/fastcs/panda/_block.py +9 -9
- ophyd_async/fastcs/panda/_control.py +9 -4
- ophyd_async/fastcs/panda/_hdf_panda.py +7 -2
- ophyd_async/fastcs/panda/_table.py +4 -1
- ophyd_async/fastcs/panda/_trigger.py +7 -7
- ophyd_async/plan_stubs/__init__.py +14 -0
- ophyd_async/plan_stubs/_ensure_connected.py +11 -17
- ophyd_async/plan_stubs/_fly.py +2 -2
- 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/sim/{demo/_sim_motor.py → _sim_motor.py} +34 -32
- ophyd_async/tango/core/_signal.py +3 -1
- ophyd_async/tango/core/_tango_transport.py +13 -15
- ophyd_async/tango/{demo → sim}/_mover.py +5 -2
- ophyd_async/testing/__init__.py +52 -0
- ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
- ophyd_async/testing/_assert.py +176 -0
- ophyd_async/{core → testing}/_mock_signal_utils.py +15 -11
- ophyd_async/testing/_one_of_everything.py +126 -0
- ophyd_async/testing/_wait_for_pending.py +22 -0
- {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0.dist-info}/METADATA +4 -2
- ophyd_async-0.9.0.dist-info/RECORD +129 -0
- {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0.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.8.0a6.dist-info/RECORD +0 -116
- ophyd_async-0.8.0a6.dist-info/entry_points.txt +0 -2
- /ophyd_async/epics/{demo → sim}/__init__.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/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.8.0a6.dist-info → ophyd_async-0.9.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -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)
|
|
@@ -37,8 +37,8 @@ class Mover(StandardReadable, Movable, Stoppable):
|
|
|
37
37
|
|
|
38
38
|
super().__init__(name=name)
|
|
39
39
|
|
|
40
|
-
def set_name(self, name: str):
|
|
41
|
-
super().set_name(name)
|
|
40
|
+
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
|
|
41
|
+
super().set_name(name, child_name_separator=child_name_separator)
|
|
42
42
|
# Readback should be named the same as its parent in read()
|
|
43
43
|
self.readback.set_name(name)
|
|
44
44
|
|
|
@@ -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(
|
|
@@ -15,9 +15,9 @@ class EnergyMode(StrictEnum):
|
|
|
15
15
|
"""Energy mode for `Sensor`"""
|
|
16
16
|
|
|
17
17
|
#: Low energy mode
|
|
18
|
-
|
|
18
|
+
LOW = "Low Energy"
|
|
19
19
|
#: High energy mode
|
|
20
|
-
|
|
20
|
+
HIGH = "High Energy"
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class Sensor(StandardReadable, EpicsDevice):
|
|
@@ -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,48 +1,50 @@
|
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
class EpicsTestEnum(StrictEnum):
|
|
18
|
+
A = "Aaa"
|
|
19
|
+
B = "Bbb"
|
|
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")]
|
|
41
|
+
longstr: A[SignalRW[str], PvSuffix("longstr")]
|
|
42
|
+
longstr2: A[SignalRW[str], PvSuffix("longstr2.VAL$")]
|
|
43
43
|
my_bool: A[SignalRW[bool], PvSuffix("bool")]
|
|
44
|
-
enum: A[SignalRW[
|
|
45
|
-
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")]
|
|
47
|
+
enum_str_fallback: A[SignalRW[str], PvSuffix("enum_str_fallback")]
|
|
46
48
|
bool_unnamed: A[SignalRW[bool], PvSuffix("bool_unnamed")]
|
|
47
49
|
partialint: A[SignalRW[int], PvSuffix("partialint")]
|
|
48
50
|
lessint: A[SignalRW[int], PvSuffix("lessint")]
|
|
@@ -54,52 +56,36 @@ class ExampleCaDevice(EpicsDevice):
|
|
|
54
56
|
stra: A[SignalRW[Sequence[str]], PvSuffix("stra")]
|
|
55
57
|
|
|
56
58
|
|
|
57
|
-
class
|
|
59
|
+
class EpicsTestPvaDevice(EpicsTestCaDevice):
|
|
60
|
+
# pva can support all signal types that ca can
|
|
58
61
|
int8a: A[SignalRW[Array1D[np.int8]], PvSuffix("int8a")]
|
|
59
62
|
uint16a: A[SignalRW[Array1D[np.uint16]], PvSuffix("uint16a")]
|
|
60
63
|
uint32a: A[SignalRW[Array1D[np.uint32]], PvSuffix("uint32a")]
|
|
61
64
|
int64a: A[SignalRW[Array1D[np.int64]], PvSuffix("int64a")]
|
|
62
65
|
uint64a: A[SignalRW[Array1D[np.uint64]], PvSuffix("uint64a")]
|
|
63
|
-
table: A[SignalRW[
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return device
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def get_example_ioc() -> TestingIOC:
|
|
93
|
-
"""Get TestingIOC instance with the example databases loaded.
|
|
94
|
-
|
|
95
|
-
Returns
|
|
96
|
-
-------
|
|
97
|
-
TestingIOC
|
|
98
|
-
instance with test_records.db loaded for ExampleCaDevice and
|
|
99
|
-
test_records.db and test_records_pva.db loaded for ExamplePvaDevice.
|
|
100
|
-
"""
|
|
101
|
-
ioc = TestingIOC()
|
|
102
|
-
ioc.database_for(PVA_RECORDS, ExamplePvaDevice)
|
|
103
|
-
ioc.database_for(CA_PVA_RECORDS, ExamplePvaDevice)
|
|
104
|
-
ioc.database_for(CA_PVA_RECORDS, ExampleCaDevice)
|
|
105
|
-
return ioc
|
|
66
|
+
table: A[SignalRW[EpicsTestTable], PvSuffix("table")]
|
|
67
|
+
ntndarray: A[SignalR[np.ndarray], PvSuffix("ntndarray")]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class EpicsTestIocAndDevices:
|
|
71
|
+
def __init__(self):
|
|
72
|
+
self.prefix = generate_random_pv_prefix()
|
|
73
|
+
self.ioc = TestingIOC()
|
|
74
|
+
# Create supporting records and ExampleCaDevice
|
|
75
|
+
ca_prefix = f"{self.prefix}ca:"
|
|
76
|
+
self.ioc.add_database(CA_PVA_RECORDS, device=ca_prefix)
|
|
77
|
+
self.ca_device = EpicsTestCaDevice(f"ca://{ca_prefix}")
|
|
78
|
+
# Create supporting records and ExamplePvaDevice
|
|
79
|
+
pva_prefix = f"{self.prefix}pva:"
|
|
80
|
+
self.ioc.add_database(CA_PVA_RECORDS, device=pva_prefix)
|
|
81
|
+
self.ioc.add_database(PVA_RECORDS, device=pva_prefix)
|
|
82
|
+
self.pva_device = EpicsTestPvaDevice(f"pva://{pva_prefix}")
|
|
83
|
+
|
|
84
|
+
def get_device(self, protocol: str) -> EpicsTestCaDevice | EpicsTestPvaDevice:
|
|
85
|
+
return getattr(self, f"{protocol}_device")
|
|
86
|
+
|
|
87
|
+
def get_signal(self, protocol: str, name: str) -> SignalRW:
|
|
88
|
+
return getattr(self.get_device(protocol), name)
|
|
89
|
+
|
|
90
|
+
def get_pv(self, protocol: str, name: str) -> str:
|
|
91
|
+
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,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")
|
|
@@ -150,3 +166,9 @@ record(lsi, "$(device)longstr2") {
|
|
|
150
166
|
field(INP, {const:"a string that is just longer than forty characters"})
|
|
151
167
|
field(PINI, "YES")
|
|
152
168
|
}
|
|
169
|
+
|
|
170
|
+
record(calc, "$(device)ticking") {
|
|
171
|
+
field(INPA, "$(device)ticking")
|
|
172
|
+
field(CALC, "A+1")
|
|
173
|
+
field(SCAN, ".1 second")
|
|
174
|
+
}
|
ophyd_async/fastcs/core.py
CHANGED
|
@@ -2,8 +2,8 @@ from ophyd_async.core import Device, DeviceConnector
|
|
|
2
2
|
from ophyd_async.epics.core import PviDeviceConnector
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def fastcs_connector(device: Device, uri: str) -> DeviceConnector:
|
|
5
|
+
def fastcs_connector(device: Device, uri: str, error_hint: str = "") -> DeviceConnector:
|
|
6
6
|
# TODO: add Tango support based on uri scheme
|
|
7
|
-
connector = PviDeviceConnector(uri)
|
|
7
|
+
connector = PviDeviceConnector(uri, error_hint)
|
|
8
8
|
connector.create_children_from_annotations(device)
|
|
9
9
|
return connector
|
|
@@ -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
|
]
|
|
@@ -36,14 +36,14 @@ class PulseBlock(Device):
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
class PcompDirection(StrictEnum):
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
POSITIVE = "Positive"
|
|
40
|
+
NEGATIVE = "Negative"
|
|
41
|
+
EITHER = "Either"
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
class BitMux(SubsetEnum):
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
ZERO = "ZERO"
|
|
46
|
+
ONE = "ONE"
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
class PcompBlock(Device):
|
|
@@ -57,10 +57,10 @@ class PcompBlock(Device):
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
class TimeUnits(StrictEnum):
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
MIN = "min"
|
|
61
|
+
S = "s"
|
|
62
|
+
MS = "ms"
|
|
63
|
+
US = "us"
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
class SeqBlock(Device):
|
|
@@ -18,10 +18,15 @@ class PandaPcapController(DetectorController):
|
|
|
18
18
|
return 0.000000008
|
|
19
19
|
|
|
20
20
|
async def prepare(self, trigger_info: TriggerInfo):
|
|
21
|
-
|
|
22
|
-
DetectorTrigger.
|
|
23
|
-
DetectorTrigger.
|
|
24
|
-
)
|
|
21
|
+
if trigger_info.trigger not in (
|
|
22
|
+
DetectorTrigger.CONSTANT_GATE,
|
|
23
|
+
DetectorTrigger.VARIABLE_GATE,
|
|
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)
|
|
@@ -9,8 +9,12 @@ from ._block import CommonPandaBlocks
|
|
|
9
9
|
from ._control import PandaPcapController
|
|
10
10
|
from ._writer import PandaHDFWriter
|
|
11
11
|
|
|
12
|
+
MINIMUM_PANDA_IOC = "0.11.4"
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
class HDFPanda(
|
|
16
|
+
CommonPandaBlocks, StandardDetector[PandaPcapController, PandaHDFWriter]
|
|
17
|
+
):
|
|
14
18
|
def __init__(
|
|
15
19
|
self,
|
|
16
20
|
prefix: str,
|
|
@@ -18,8 +22,9 @@ class HDFPanda(CommonPandaBlocks, StandardDetector):
|
|
|
18
22
|
config_sigs: Sequence[SignalR] = (),
|
|
19
23
|
name: str = "",
|
|
20
24
|
):
|
|
25
|
+
error_hint = f"Is PandABlocks-ioc at least version {MINIMUM_PANDA_IOC}?"
|
|
21
26
|
# This has to be first so we make self.pcap
|
|
22
|
-
connector = fastcs_connector(self, prefix)
|
|
27
|
+
connector = fastcs_connector(self, prefix, error_hint)
|
|
23
28
|
controller = PandaPcapController(pcap=self.pcap)
|
|
24
29
|
writer = PandaHDFWriter(
|
|
25
30
|
path_provider=path_provider,
|
|
@@ -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
|
|
@@ -20,8 +20,8 @@ class StaticSeqTableTriggerLogic(FlyerController[SeqTableInfo]):
|
|
|
20
20
|
|
|
21
21
|
async def prepare(self, value: SeqTableInfo):
|
|
22
22
|
await asyncio.gather(
|
|
23
|
-
self.seq.prescale_units.set(TimeUnits.
|
|
24
|
-
self.seq.enable.set(BitMux.
|
|
23
|
+
self.seq.prescale_units.set(TimeUnits.US),
|
|
24
|
+
self.seq.enable.set(BitMux.ZERO),
|
|
25
25
|
)
|
|
26
26
|
await asyncio.gather(
|
|
27
27
|
self.seq.prescale.set(value.prescale_as_us),
|
|
@@ -30,14 +30,14 @@ class StaticSeqTableTriggerLogic(FlyerController[SeqTableInfo]):
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
async def kickoff(self) -> None:
|
|
33
|
-
await self.seq.enable.set(BitMux.
|
|
33
|
+
await self.seq.enable.set(BitMux.ONE)
|
|
34
34
|
await wait_for_value(self.seq.active, True, timeout=1)
|
|
35
35
|
|
|
36
36
|
async def complete(self) -> None:
|
|
37
37
|
await wait_for_value(self.seq.active, False, timeout=None)
|
|
38
38
|
|
|
39
39
|
async def stop(self):
|
|
40
|
-
await self.seq.enable.set(BitMux.
|
|
40
|
+
await self.seq.enable.set(BitMux.ZERO)
|
|
41
41
|
await wait_for_value(self.seq.active, False, timeout=1)
|
|
42
42
|
|
|
43
43
|
|
|
@@ -68,7 +68,7 @@ class StaticPcompTriggerLogic(FlyerController[PcompInfo]):
|
|
|
68
68
|
self.pcomp = pcomp
|
|
69
69
|
|
|
70
70
|
async def prepare(self, value: PcompInfo):
|
|
71
|
-
await self.pcomp.enable.set(BitMux.
|
|
71
|
+
await self.pcomp.enable.set(BitMux.ZERO)
|
|
72
72
|
await asyncio.gather(
|
|
73
73
|
self.pcomp.start.set(value.start_postion),
|
|
74
74
|
self.pcomp.width.set(value.pulse_width),
|
|
@@ -78,12 +78,12 @@ class StaticPcompTriggerLogic(FlyerController[PcompInfo]):
|
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
async def kickoff(self) -> None:
|
|
81
|
-
await self.pcomp.enable.set(BitMux.
|
|
81
|
+
await self.pcomp.enable.set(BitMux.ONE)
|
|
82
82
|
await wait_for_value(self.pcomp.active, True, timeout=1)
|
|
83
83
|
|
|
84
84
|
async def complete(self, timeout: float | None = None) -> None:
|
|
85
85
|
await wait_for_value(self.pcomp.active, False, timeout=timeout)
|
|
86
86
|
|
|
87
87
|
async def stop(self):
|
|
88
|
-
await self.pcomp.enable.set(BitMux.
|
|
88
|
+
await self.pcomp.enable.set(BitMux.ZERO)
|
|
89
89
|
await wait_for_value(self.pcomp.active, False, timeout=1)
|
|
@@ -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,11 +58,11 @@ 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,
|
|
65
|
-
trigger=DetectorTrigger.
|
|
65
|
+
trigger=DetectorTrigger.CONSTANT_GATE,
|
|
66
66
|
deadtime=deadtime,
|
|
67
67
|
livetime=exposure,
|
|
68
68
|
frame_timeout=frame_timeout,
|