From c2fa9d50f6a51610237a7cfff7e8d8170217346a Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Tue, 23 Apr 2024 13:41:25 +0100 Subject: [PATCH] Make HDFPandA StandardDetector (#185) made a new class for the PandA hdf writer and adjusted `PandAController` --- src/ophyd_async/core/_providers.py | 4 +- src/ophyd_async/epics/pvi/__init__.py | 4 +- src/ophyd_async/epics/pvi/pvi.py | 45 +++- src/ophyd_async/panda/__init__.py | 15 +- src/ophyd_async/panda/_common_blocks.py | 49 ++++ src/ophyd_async/panda/_hdf_panda.py | 48 ++++ ...nda_controller.py => _panda_controller.py} | 10 +- src/ophyd_async/panda/{table.py => _table.py} | 0 .../panda/{trigger.py => _trigger.py} | 0 src/ophyd_async/panda/{utils.py => _utils.py} | 0 src/ophyd_async/panda/panda.py | 74 ------ src/ophyd_async/panda/writers/__init__.py | 2 +- .../writers/{hdf_writer.py => _hdf_writer.py} | 14 +- .../{panda_hdf_file.py => _panda_hdf_file.py} | 0 .../planstubs/prepare_trigger_and_dets.py | 4 +- tests/epics/test_pvi.py | 48 +++- tests/panda/test_hdf_panda.py | 210 ++++++++++++++++++ .../{test_panda.py => test_panda_connect.py} | 83 +++---- tests/panda/test_panda_controller.py | 38 +++- tests/panda/test_panda_utils.py | 24 +- tests/panda/test_table.py | 2 +- tests/panda/test_trigger.py | 20 +- tests/panda/test_writer.py | 93 ++++---- tests/test_flyer_with_panda.py | 16 +- 24 files changed, 590 insertions(+), 213 deletions(-) create mode 100644 src/ophyd_async/panda/_common_blocks.py create mode 100644 src/ophyd_async/panda/_hdf_panda.py rename src/ophyd_async/panda/{panda_controller.py => _panda_controller.py} (89%) rename src/ophyd_async/panda/{table.py => _table.py} (100%) rename src/ophyd_async/panda/{trigger.py => _trigger.py} (100%) rename src/ophyd_async/panda/{utils.py => _utils.py} (100%) delete mode 100644 src/ophyd_async/panda/panda.py rename src/ophyd_async/panda/writers/{hdf_writer.py => _hdf_writer.py} (95%) rename src/ophyd_async/panda/writers/{panda_hdf_file.py => _panda_hdf_file.py} (100%) create mode 100644 tests/panda/test_hdf_panda.py rename tests/panda/{test_panda.py => test_panda_connect.py} (69%) diff --git a/src/ophyd_async/core/_providers.py b/src/ophyd_async/core/_providers.py index 9805f8bc55..091ea387de 100644 --- a/src/ophyd_async/core/_providers.py +++ b/src/ophyd_async/core/_providers.py @@ -39,8 +39,10 @@ def __init__( directory_path: Union[str, Path], filename_prefix: str = "", filename_suffix: str = "", - resource_dir: Path = Path("."), + resource_dir: Optional[Path] = None, ) -> None: + if resource_dir is None: + resource_dir = Path(".") if isinstance(directory_path, str): directory_path = Path(directory_path) self._directory_info = DirectoryInfo( diff --git a/src/ophyd_async/epics/pvi/__init__.py b/src/ophyd_async/epics/pvi/__init__.py index 307c3b35ef..ad638b740c 100644 --- a/src/ophyd_async/epics/pvi/__init__.py +++ b/src/ophyd_async/epics/pvi/__init__.py @@ -1,3 +1,3 @@ -from .pvi import PVIEntry, fill_pvi_entries +from .pvi import PVIEntry, create_children_from_annotations, fill_pvi_entries -__all__ = ["PVIEntry", "fill_pvi_entries"] +__all__ = ["PVIEntry", "fill_pvi_entries", "create_children_from_annotations"] diff --git a/src/ophyd_async/epics/pvi/pvi.py b/src/ophyd_async/epics/pvi/pvi.py index 881d4f6b24..e68cf3b132 100644 --- a/src/ophyd_async/epics/pvi/pvi.py +++ b/src/ophyd_async/epics/pvi/pvi.py @@ -1,5 +1,6 @@ import re from dataclasses import dataclass +from inspect import isclass from typing import ( Any, Callable, @@ -56,13 +57,14 @@ def _split_subscript(tp: T) -> Union[Tuple[Any, Tuple[Any]], Tuple[T, None]]: return tp, None -def _strip_union(field: Union[Union[T], T]) -> T: +def _strip_union(field: Union[Union[T], T]) -> Tuple[T, bool]: if get_origin(field) is Union: args = get_args(field) + is_optional = type(None) in args for arg in args: if arg is not type(None): - return arg - return field + return arg, is_optional + return field, False def _strip_device_vector(field: Union[Type[Device]]) -> Tuple[bool, Type[Device]]: @@ -92,9 +94,10 @@ def _verify_common_blocks(entry: PVIEntry, common_device: Type[Device]): if sub_name in ("_name", "parent"): continue assert entry.sub_entries - if sub_name not in entry.sub_entries and get_origin(sub_device) is not Optional: + device_t, is_optional = _strip_union(sub_device) + if sub_name not in entry.sub_entries and not is_optional: raise RuntimeError( - f"sub device `{sub_name}:{type(sub_device)}` was not provided by pvi" + f"sub device `{sub_name}:{type(sub_device)}` " "was not provided by pvi" ) if isinstance(entry.sub_entries[sub_name], dict): for sub_sub_entry in entry.sub_entries[sub_name].values(): # type: ignore @@ -128,7 +131,7 @@ def _parse_type( ): if common_device_type: # pre-defined type - device_cls = _strip_union(common_device_type) + device_cls, _ = _strip_union(common_device_type) is_device_vector, device_cls = _strip_device_vector(device_cls) device_cls, device_args = _split_subscript(device_cls) assert issubclass(device_cls, Device) @@ -162,7 +165,7 @@ def _sim_common_blocks(device: Device, stripped_type: Optional[Type] = None): ) for device_name, device_cls in sub_devices: - device_cls = _strip_union(device_cls) + device_cls, _ = _strip_union(device_cls) is_device_vector, device_cls = _strip_device_vector(device_cls) device_cls, device_args = _split_subscript(device_cls) assert issubclass(device_cls, Device) @@ -187,8 +190,7 @@ def _sim_common_blocks(device: Device, stripped_type: Optional[Type] = None): if is_signal: sub_device = device_cls(SimSignalBackend(signal_dtype)) else: - sub_device = device_cls() - + sub_device = getattr(device, device_name, device_cls()) _sim_common_blocks(sub_device, stripped_type=device_cls) setattr(device, device_name, sub_device) @@ -223,7 +225,7 @@ async def _get_pvi_entries(entry: PVIEntry, timeout=DEFAULT_TIMEOUT): if is_signal: device = _pvi_mapping[frozenset(pva_entries.keys())](signal_dtype, *pvs) else: - device = device_type() + device = getattr(entry.device, sub_name, device_type()) sub_entry = PVIEntry( device=device, common_device_type=device_type, sub_entries={} @@ -291,3 +293,26 @@ async def fill_pvi_entries( # We call set name now the parent field has been set in all of the # introspect-initialized devices. This will recursively set the names. device.set_name(device.name) + + +def create_children_from_annotations( + device: Device, included_optional_fields: Tuple[str, ...] = () +): + """For intializing blocks at __init__ of ``device``.""" + for name, device_type in get_type_hints(type(device)).items(): + if name in ("_name", "parent"): + continue + device_type, is_optional = _strip_union(device_type) + if is_optional and name not in included_optional_fields: + continue + is_device_vector, device_type = _strip_device_vector(device_type) + if ( + is_device_vector + or ((origin := get_origin(device_type)) and issubclass(origin, Signal)) + or (isclass(device_type) and issubclass(device_type, Signal)) + ): + continue + + sub_device = device_type() + setattr(device, name, sub_device) + create_children_from_annotations(sub_device) diff --git a/src/ophyd_async/panda/__init__.py b/src/ophyd_async/panda/__init__.py index 9c572f52c0..f2170263af 100644 --- a/src/ophyd_async/panda/__init__.py +++ b/src/ophyd_async/panda/__init__.py @@ -1,24 +1,25 @@ -from .panda import ( - CommonPandABlocks, +from ._common_blocks import ( + CommonPandaBlocks, DataBlock, - PandA, PcapBlock, PulseBlock, SeqBlock, TimeUnits, ) -from .panda_controller import PandaPcapController -from .table import ( +from ._hdf_panda import HDFPanda +from ._panda_controller import PandaPcapController +from ._table import ( SeqTable, SeqTableRow, SeqTrigger, seq_table_from_arrays, seq_table_from_rows, ) -from .utils import phase_sorter +from ._utils import phase_sorter __all__ = [ - "PandA", + "CommonPandaBlocks", + "HDFPanda", "PcapBlock", "PulseBlock", "seq_table_from_arrays", diff --git a/src/ophyd_async/panda/_common_blocks.py b/src/ophyd_async/panda/_common_blocks.py new file mode 100644 index 0000000000..9c8501f52f --- /dev/null +++ b/src/ophyd_async/panda/_common_blocks.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from enum import Enum + +from ophyd_async.core import Device, DeviceVector, SignalR, SignalRW +from ophyd_async.panda._table import SeqTable + + +class DataBlock(Device): + # In future we may decide to make hdf_* optional + hdf_directory: SignalRW[str] + hdf_file_name: SignalRW[str] + num_capture: SignalRW[int] + num_captured: SignalR[int] + capture: SignalRW[bool] + flush_period: SignalRW[float] + + +class PulseBlock(Device): + delay: SignalRW[float] + width: SignalRW[float] + + +class TimeUnits(str, Enum): + min = "min" + s = "s" + ms = "ms" + us = "us" + + +class SeqBlock(Device): + table: SignalRW[SeqTable] + active: SignalRW[bool] + repeats: SignalRW[int] + prescale: SignalRW[float] + prescale_units: SignalRW[TimeUnits] + enable: SignalRW[str] + + +class PcapBlock(Device): + active: SignalR[bool] + arm: SignalRW[bool] + + +class CommonPandaBlocks(Device): + pulse: DeviceVector[PulseBlock] + seq: DeviceVector[SeqBlock] + pcap: PcapBlock + data: DataBlock diff --git a/src/ophyd_async/panda/_hdf_panda.py b/src/ophyd_async/panda/_hdf_panda.py new file mode 100644 index 0000000000..75c483e031 --- /dev/null +++ b/src/ophyd_async/panda/_hdf_panda.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from typing import Sequence + +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + DirectoryProvider, + SignalR, + StandardDetector, +) +from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries + +from ._common_blocks import CommonPandaBlocks +from ._panda_controller import PandaPcapController +from .writers._hdf_writer import PandaHDFWriter + + +class HDFPanda(CommonPandaBlocks, StandardDetector): + def __init__( + self, + prefix: str, + directory_provider: DirectoryProvider, + config_sigs: Sequence[SignalR] = (), + name: str = "", + ): + self._prefix = prefix + + create_children_from_annotations(self) + controller = PandaPcapController(pcap=self.pcap) + writer = PandaHDFWriter( + prefix=prefix, + directory_provider=directory_provider, + name_provider=lambda: name, + panda_device=self, + ) + super().__init__( + controller=controller, + writer=writer, + config_sigs=config_sigs, + name=name, + writer_timeout=DEFAULT_TIMEOUT, + ) + + async def connect( + self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT + ) -> None: + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim=sim, timeout=timeout) diff --git a/src/ophyd_async/panda/panda_controller.py b/src/ophyd_async/panda/_panda_controller.py similarity index 89% rename from src/ophyd_async/panda/panda_controller.py rename to src/ophyd_async/panda/_panda_controller.py index 2c1100bfff..6000909576 100644 --- a/src/ophyd_async/panda/panda_controller.py +++ b/src/ophyd_async/panda/_panda_controller.py @@ -7,15 +7,11 @@ DetectorTrigger, wait_for_value, ) - -from .panda import PcapBlock +from ophyd_async.panda import PcapBlock class PandaPcapController(DetectorControl): - def __init__( - self, - pcap: PcapBlock, - ) -> None: + def __init__(self, pcap: PcapBlock) -> None: self.pcap = pcap def get_deadtime(self, exposure: float) -> float: @@ -35,7 +31,7 @@ async def arm( await wait_for_value(self.pcap.active, True, timeout=1) return AsyncStatus(wait_for_value(self.pcap.active, False, timeout=None)) - async def disarm(self): + async def disarm(self) -> AsyncStatus: await asyncio.gather(self.pcap.arm.set(False)) await wait_for_value(self.pcap.active, False, timeout=1) return AsyncStatus(wait_for_value(self.pcap.active, False, timeout=None)) diff --git a/src/ophyd_async/panda/table.py b/src/ophyd_async/panda/_table.py similarity index 100% rename from src/ophyd_async/panda/table.py rename to src/ophyd_async/panda/_table.py diff --git a/src/ophyd_async/panda/trigger.py b/src/ophyd_async/panda/_trigger.py similarity index 100% rename from src/ophyd_async/panda/trigger.py rename to src/ophyd_async/panda/_trigger.py diff --git a/src/ophyd_async/panda/utils.py b/src/ophyd_async/panda/_utils.py similarity index 100% rename from src/ophyd_async/panda/utils.py rename to src/ophyd_async/panda/_utils.py diff --git a/src/ophyd_async/panda/panda.py b/src/ophyd_async/panda/panda.py deleted file mode 100644 index d579877228..0000000000 --- a/src/ophyd_async/panda/panda.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -from enum import Enum - -from ophyd_async.core import DEFAULT_TIMEOUT, Device, DeviceVector, SignalR, SignalRW -from ophyd_async.epics.pvi import fill_pvi_entries -from ophyd_async.panda.table import SeqTable - - -class DataBlock(Device): - hdf_directory: SignalRW[str] - hdf_file_name: SignalRW[str] - num_capture: SignalRW[int] - num_captured: SignalR[int] - capture: SignalRW[bool] - flush_period: SignalRW[float] - - -class PulseBlock(Device): - delay: SignalRW[float] - width: SignalRW[float] - - -class TimeUnits(str, Enum): - min = "min" - s = "s" - ms = "ms" - us = "us" - - -class SeqBlock(Device): - table: SignalRW[SeqTable] - active: SignalRW[bool] - repeats: SignalRW[int] - prescale: SignalRW[float] - prescale_units: SignalRW[TimeUnits] - enable: SignalRW[str] - - -class PcapBlock(Device): - active: SignalR[bool] - arm: SignalRW[bool] - - -class CommonPandABlocks(Device): - pulse: DeviceVector[PulseBlock] - seq: DeviceVector[SeqBlock] - pcap: PcapBlock - - -class PandA(CommonPandABlocks): - data: DataBlock - - def __init__(self, prefix: str, name: str = "") -> None: - self._prefix = prefix - # Remove this assert once PandA IOC supports different prefixes - assert prefix.endswith(":"), f"PandA prefix '{prefix}' must end in ':'" - super().__init__(name) - - async def connect( - self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT - ) -> None: - """Initialises all blocks and connects them. - - First, checks for pvi information. If it exists, make all blocks from this. - Then, checks that all required blocks in the PandA have been made. - - If there's no pvi information, that's because we're in sim mode. In that case, - makes all required blocks. - """ - - await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) - - await super().connect(sim) diff --git a/src/ophyd_async/panda/writers/__init__.py b/src/ophyd_async/panda/writers/__init__.py index a623a16a16..7cc7974ea7 100644 --- a/src/ophyd_async/panda/writers/__init__.py +++ b/src/ophyd_async/panda/writers/__init__.py @@ -1,3 +1,3 @@ -from .hdf_writer import PandaHDFWriter +from ._hdf_writer import PandaHDFWriter __all__ = ["PandaHDFWriter"] diff --git a/src/ophyd_async/panda/writers/hdf_writer.py b/src/ophyd_async/panda/writers/_hdf_writer.py similarity index 95% rename from src/ophyd_async/panda/writers/hdf_writer.py rename to src/ophyd_async/panda/writers/_hdf_writer.py index dc58fce101..07abdb5bf0 100644 --- a/src/ophyd_async/panda/writers/hdf_writer.py +++ b/src/ophyd_async/panda/writers/_hdf_writer.py @@ -17,9 +17,9 @@ wait_for_value, ) from ophyd_async.core.signal import observe_value -from ophyd_async.panda.panda import PandA +from ophyd_async.panda import CommonPandaBlocks -from .panda_hdf_file import _HDFDataset, _HDFFile +from ._panda_hdf_file import _HDFDataset, _HDFFile class Capture(str, Enum): @@ -96,7 +96,7 @@ def __init__( prefix: str, directory_provider: DirectoryProvider, name_provider: NameProvider, - panda_device: PandA, + panda_device: CommonPandaBlocks, ) -> None: self.panda_device = panda_device self._prefix = prefix @@ -142,11 +142,11 @@ async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]: for attribute_path, capture_signal in to_capture.items(): split_path = attribute_path.split(".") signal_name = split_path[-1] + # Get block names from numbered blocks, eg INENC[1] block_name = ( - split_path[-2] - if not split_path[-2].isnumeric() - # Get block names from numbered blocks, eg INENC[1] - else f"{split_path[-3]}{split_path[-2]}" + f"{split_path[-3]}{split_path[-2]}" + if split_path[-2].isnumeric() + else split_path[-2] ) for suffix in str(capture_signal.capture_type).split(" "): diff --git a/src/ophyd_async/panda/writers/panda_hdf_file.py b/src/ophyd_async/panda/writers/_panda_hdf_file.py similarity index 100% rename from src/ophyd_async/panda/writers/panda_hdf_file.py rename to src/ophyd_async/panda/writers/_panda_hdf_file.py diff --git a/src/ophyd_async/planstubs/prepare_trigger_and_dets.py b/src/ophyd_async/planstubs/prepare_trigger_and_dets.py index 08e481b74e..541c61faac 100644 --- a/src/ophyd_async/planstubs/prepare_trigger_and_dets.py +++ b/src/ophyd_async/planstubs/prepare_trigger_and_dets.py @@ -5,8 +5,8 @@ from ophyd_async.core.detector import DetectorTrigger, StandardDetector, TriggerInfo from ophyd_async.core.flyer import HardwareTriggeredFlyable from ophyd_async.core.utils import in_micros -from ophyd_async.panda.table import SeqTable, SeqTableRow, seq_table_from_rows -from ophyd_async.panda.trigger import SeqTableInfo +from ophyd_async.panda._table import SeqTable, SeqTableRow, seq_table_from_rows +from ophyd_async.panda._trigger import SeqTableInfo def prepare_static_seq_table_flyer_and_detectors_with_same_trigger( diff --git a/tests/epics/test_pvi.py b/tests/epics/test_pvi.py index 6f29441254..65574f14dc 100644 --- a/tests/epics/test_pvi.py +++ b/tests/epics/test_pvi.py @@ -10,7 +10,7 @@ SignalRW, SignalX, ) -from ophyd_async.epics.pvi import fill_pvi_entries +from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries class Block1(Device): @@ -94,3 +94,49 @@ async def test_fill_pvi_entries_sim_mode(pvi_test_device_t): # top level signals are typed assert test_device.signal_rw._backend.datatype is int + + +@pytest.fixture +def pvi_test_device_create_children_from_annotations_t(): + """A fixture since pytest discourages init in test case classes""" + + class TestDevice(Block3, Device): + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + super().__init__(name) + create_children_from_annotations(self) + + async def connect( + self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT + ) -> None: + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + + await super().connect(sim) + + yield TestDevice + + +async def test_device_create_children_from_annotations( + pvi_test_device_create_children_from_annotations_t, +): + device = pvi_test_device_create_children_from_annotations_t("PREFIX:") + + block_2_device = device.device + block_1_device = device.device.device + top_block_1_device = device.signal_device + + # The create_children_from_annotations has only made blocks, + # not signals or device vectors + assert isinstance(block_2_device, Block2) + assert isinstance(block_1_device, Block1) + assert isinstance(top_block_1_device, Block1) + assert not hasattr(device, "signal_x") + assert not hasattr(device, "signal_rw") + assert not hasattr(top_block_1_device, "signal_rw") + + await device.connect(sim=True) + + # The memory addresses have not changed + assert device.device is block_2_device + assert device.device.device is block_1_device + assert device.signal_device is top_block_1_device diff --git a/tests/panda/test_hdf_panda.py b/tests/panda/test_hdf_panda.py new file mode 100644 index 0000000000..2c376fb12f --- /dev/null +++ b/tests/panda/test_hdf_panda.py @@ -0,0 +1,210 @@ +import asyncio +from typing import Dict, Optional + +import pytest +from bluesky import plan_stubs as bps +from bluesky.run_engine import RunEngine + +from ophyd_async.core import StaticDirectoryProvider, set_sim_value +from ophyd_async.core.async_status import AsyncStatus +from ophyd_async.core.detector import DetectorControl, DetectorTrigger +from ophyd_async.core.device import Device +from ophyd_async.core.flyer import HardwareTriggeredFlyable +from ophyd_async.core.signal import SignalR, wait_for_value +from ophyd_async.core.sim_signal_backend import SimSignalBackend +from ophyd_async.core.utils import DEFAULT_TIMEOUT +from ophyd_async.panda import HDFPanda, PcapBlock +from ophyd_async.panda._trigger import StaticSeqTableTriggerLogic +from ophyd_async.panda.writers._hdf_writer import Capture +from ophyd_async.planstubs.prepare_trigger_and_dets import ( + prepare_static_seq_table_flyer_and_detectors_with_same_trigger, +) + + +def assert_emitted(docs: Dict[str, list], **numbers: int): + assert list(docs) == list(numbers) + assert {name: len(d) for name, d in docs.items()} == numbers + + +class MockPandaPcapController(DetectorControl): + def __init__(self, pcap: PcapBlock) -> None: + self.pcap = pcap + + def get_deadtime(self, exposure: float) -> float: + return 0.000000008 + + async def arm( + self, + num: int, + trigger: DetectorTrigger = DetectorTrigger.constant_gate, + exposure: Optional[float] = None, + timeout=DEFAULT_TIMEOUT, + ) -> AsyncStatus: + assert trigger in ( + DetectorTrigger.constant_gate, + trigger == DetectorTrigger.variable_gate, + ), ( + f"Receieved trigger {trigger}. Only constant_gate and " + "variable_gate triggering is supported on the PandA" + ) + await self.pcap.arm.set(True, wait=True, timeout=timeout) + await wait_for_value(self.pcap.active, True, timeout=timeout) + await asyncio.sleep(0.2) + await self.pcap.arm.set(False, wait=False, timeout=timeout) + return AsyncStatus(wait_for_value(self.pcap.active, False, timeout=None)) + + async def disarm(self, timeout=DEFAULT_TIMEOUT) -> AsyncStatus: + await self.pcap.arm.set(False, wait=True, timeout=timeout) + await wait_for_value(self.pcap.active, False, timeout=timeout) + await asyncio.sleep(0.2) + set_sim_value(self.pcap.active, True) + return AsyncStatus(wait_for_value(self.pcap.active, False, timeout=None)) + + +@pytest.fixture +async def sim_hdf_panda(tmp_path): + class CaptureBlock(Device): + test_capture: SignalR + + directory_provider = StaticDirectoryProvider(str(tmp_path), filename_prefix="test") + sim_hdf_panda = HDFPanda( + "HDFPANDA:", directory_provider=directory_provider, name="panda" + ) + sim_hdf_panda._controller = MockPandaPcapController(sim_hdf_panda.pcap) + block_a = CaptureBlock(name="block_a") + block_b = CaptureBlock(name="block_b") + block_a.test_capture = SignalR(backend=SimSignalBackend(Capture)) + block_b.test_capture = SignalR(backend=SimSignalBackend(Capture)) + + setattr(sim_hdf_panda, "block_a", block_a) + setattr(sim_hdf_panda, "block_b", block_b) + await sim_hdf_panda.connect(sim=True) + set_sim_value(block_a.test_capture, Capture.Min) + set_sim_value(block_b.test_capture, Capture.Diff) + + yield sim_hdf_panda + + +async def test_hdf_panda_passes_blocks_to_controller(sim_hdf_panda: HDFPanda): + assert hasattr(sim_hdf_panda.controller, "pcap") + assert sim_hdf_panda.controller.pcap is sim_hdf_panda.pcap + + +async def test_hdf_panda_hardware_triggered_flyable( + RE: RunEngine, + sim_hdf_panda, +): + docs = {} + + def append_and_print(name, doc): + if name not in docs: + docs[name] = [] + docs[name] += [doc] + + RE.subscribe(append_and_print) + + shutter_time = 0.004 + exposure = 1 + + trigger_logic = StaticSeqTableTriggerLogic(sim_hdf_panda.seq[1]) + flyer = HardwareTriggeredFlyable(trigger_logic, [], name="flyer") + + def flying_plan(): + yield from bps.stage_all(sim_hdf_panda, flyer) + + yield from prepare_static_seq_table_flyer_and_detectors_with_same_trigger( + flyer, + [sim_hdf_panda], + num=1, + width=exposure, + deadtime=sim_hdf_panda.controller.get_deadtime(1), + shutter_time=shutter_time, + ) + # sim_hdf_panda.controller.disarm.assert_called_once # type: ignore + + yield from bps.open_run() + yield from bps.declare_stream(sim_hdf_panda, name="main_stream", collect=True) + + set_sim_value(flyer.trigger_logic.seq.active, 1) + + yield from bps.kickoff(flyer, wait=True) + yield from bps.kickoff(sim_hdf_panda) + + yield from bps.complete(flyer, wait=False, group="complete") + yield from bps.complete(sim_hdf_panda, wait=False, group="complete") + + # Manually incremenet the index as if a frame was taken + set_sim_value( + sim_hdf_panda.data.num_captured, + sim_hdf_panda.data.num_captured._backend._value + 1, + ) + + set_sim_value(flyer.trigger_logic.seq.active, 0) + + done = False + while not done: + try: + yield from bps.wait(group="complete", timeout=0.5) + except TimeoutError: + pass + else: + done = True + yield from bps.collect( + sim_hdf_panda, + return_payload=False, + name="main_stream", + ) + yield from bps.wait(group="complete") + yield from bps.close_run() + + yield from bps.unstage_all(flyer, sim_hdf_panda) + # assert sim_hdf_panda.controller.disarm.called # type: ignore + + # fly scan + RE(flying_plan()) + + assert_emitted( + docs, start=1, descriptor=1, stream_resource=2, stream_datum=2, stop=1 + ) + + # test descriptor + data_key_names: Dict[str, str] = docs["descriptor"][0]["object_keys"]["panda"] + assert data_key_names == [ + "panda-block_a-test-Min", + "panda-block_b-test-Diff", + ] + for data_key_name in data_key_names: + assert ( + docs["descriptor"][0]["data_keys"][data_key_name]["source"] + == "soft://panda-data-hdf_directory" + ) + + # test stream resources + for block_letter, stream_resource, data_key_name in zip( + ("a", "b"), docs["stream_resource"], data_key_names + ): + assert stream_resource["data_key"] == data_key_name + assert stream_resource["spec"] == "AD_HDF5_SWMR_SLICE" + assert stream_resource["run_start"] == docs["start"][0]["uid"] + assert stream_resource["resource_kwargs"] == { + "block": f"block_{block_letter}", + "multiplier": 1, + "name": data_key_name, + "path": f"BLOCK_{block_letter.upper()}-TEST-{data_key_name.split('-')[-1]}", + "timestamps": "/entry/instrument/NDAttributes/NDArrayTimeStamp", + } + + # test stream datum + for stream_datum in docs["stream_datum"]: + assert stream_datum["descriptor"] == docs["descriptor"][0]["uid"] + assert stream_datum["seq_nums"] == { + "start": 1, + "stop": 2, + } + assert stream_datum["indices"] == { + "start": 0, + "stop": 1, + } + assert stream_datum["stream_resource"] in [ + sd["uid"].split("/")[0] for sd in docs["stream_datum"] + ] diff --git a/tests/panda/test_panda.py b/tests/panda/test_panda_connect.py similarity index 69% rename from tests/panda/test_panda.py rename to tests/panda/test_panda_connect.py index 1259f199ab..7dcd9c5b51 100644 --- a/tests/panda/test_panda.py +++ b/tests/panda/test_panda_connect.py @@ -1,4 +1,4 @@ -"""Test file specifying how we want to eventually interact with the panda...""" +"""Used to test setting up signals for a PandA""" import copy from typing import Dict @@ -6,18 +6,11 @@ import numpy as np import pytest -from ophyd_async.core import DEFAULT_TIMEOUT, Device, DeviceCollector +from ophyd_async.core import DEFAULT_TIMEOUT, Device, DeviceCollector, DeviceVector from ophyd_async.core.utils import NotConnected from ophyd_async.epics.pvi import PVIEntry, fill_pvi_entries -from ophyd_async.panda import ( - CommonPandABlocks, - PandA, - PcapBlock, - PulseBlock, - SeqBlock, - SeqTable, - SeqTrigger, -) +from ophyd_async.epics.pvi.pvi import create_children_from_annotations +from ophyd_async.panda import PcapBlock, PulseBlock, SeqBlock, SeqTable, SeqTrigger class DummyDict: @@ -45,39 +38,45 @@ def get(self, pv: str, timeout: float = 0.0): @pytest.fixture -async def sim_panda(): - async with DeviceCollector(sim=True): - sim_panda = PandA("PANDAQSRV:", "sim_panda") +async def panda_t(): + class CommonPandaBlocksNoData(Device): + pcap: PcapBlock + pulse: DeviceVector[PulseBlock] + seq: DeviceVector[SeqBlock] - assert sim_panda.name == "sim_panda" - yield sim_panda + class Panda(CommonPandaBlocksNoData): + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + create_children_from_annotations(self) + super().__init__(name) + async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim, timeout) -class PandANoDataBlock(CommonPandABlocks): - def __init__(self, prefix: str, name: str = "") -> None: - self._prefix = prefix - assert prefix.endswith(":"), f"PandA prefix '{prefix}' must end in ':'" - super().__init__(name) + yield Panda - async def connect( - self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT - ) -> None: - await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) - await super().connect(sim) +@pytest.fixture +async def sim_panda(panda_t): + async with DeviceCollector(sim=True): + sim_panda = panda_t("PANDAQSRV:", "sim_panda") + assert sim_panda.name == "sim_panda" + yield sim_panda -def test_panda_names_correct(sim_panda: PandA): + +def test_panda_names_correct(sim_panda): assert sim_panda.seq[1].name == "sim_panda-seq-1" assert sim_panda.pulse[1].name == "sim_panda-pulse-1" -def test_panda_name_set(): - panda = PandA(":", "panda") +def test_panda_name_set(panda_t): + panda = panda_t(":", "panda") assert panda.name == "panda" -async def test_panda_children_connected(sim_panda: PandA): +async def test_panda_children_connected(sim_panda): # try to set and retrieve from simulated values... table = SeqTable( repeats=np.array([1, 1, 1, 32]).astype(np.uint16), @@ -113,8 +112,8 @@ async def test_panda_children_connected(sim_panda: PandA): assert readback_seq == table -async def test_panda_with_missing_blocks(panda_pva): - panda = PandA("PANDAQSRVI:") +async def test_panda_with_missing_blocks(panda_pva, panda_t): + panda = panda_t("PANDAQSRVI:") with pytest.raises(RuntimeError) as exc: await panda.connect() assert ( @@ -123,8 +122,8 @@ async def test_panda_with_missing_blocks(panda_pva): ) -async def test_panda_with_extra_blocks_and_signals(panda_pva): - panda = PandANoDataBlock("PANDAQSRV:") +async def test_panda_with_extra_blocks_and_signals(panda_pva, panda_t): + panda = panda_t("PANDAQSRV:") await panda.connect() assert panda.extra # type: ignore assert panda.extra[1] # type: ignore @@ -132,10 +131,14 @@ async def test_panda_with_extra_blocks_and_signals(panda_pva): assert panda.pcap.newsignal # type: ignore -async def test_panda_gets_types_from_common_class(panda_pva): - panda = PandANoDataBlock("PANDAQSRV:") +async def test_panda_gets_types_from_common_class(panda_pva, panda_t): + panda = panda_t("PANDAQSRV:") + pcap = panda.pcap await panda.connect() + # The pre-initialized blocks are now filled + assert pcap is panda.pcap + # sub devices have the correct types assert isinstance(panda.pcap, PcapBlock) assert isinstance(panda.seq[1], SeqBlock) @@ -154,8 +157,8 @@ async def test_panda_gets_types_from_common_class(panda_pva): assert panda.pcap.newsignal._backend.datatype is None -async def test_panda_block_missing_signals(panda_pva): - panda = PandA("PANDAQSRVIB:") +async def test_panda_block_missing_signals(panda_pva, panda_t): + panda = panda_t("PANDAQSRVIB:") with pytest.raises(Exception) as exc: await panda.connect() @@ -166,8 +169,8 @@ async def test_panda_block_missing_signals(panda_pva): ) -async def test_panda_unable_to_connect_to_pvi(): - panda = PandA("NON-EXISTENT:") +async def test_panda_unable_to_connect_to_pvi(panda_t): + panda = panda_t("NON-EXISTENT:") with pytest.raises(NotConnected) as exc: await panda.connect(timeout=0.01) diff --git a/tests/panda/test_panda_controller.py b/tests/panda/test_panda_controller.py index d58c7e12ea..875a4158b6 100644 --- a/tests/panda/test_panda_controller.py +++ b/tests/panda/test_panda_controller.py @@ -4,25 +4,49 @@ import pytest -from ophyd_async.core import DetectorTrigger, DeviceCollector -from ophyd_async.panda import PandA, PandaPcapController +from ophyd_async.core import DEFAULT_TIMEOUT, DetectorTrigger, Device, DeviceCollector +from ophyd_async.epics.pvi import fill_pvi_entries +from ophyd_async.epics.signal import epics_signal_rw +from ophyd_async.panda import CommonPandaBlocks, PandaPcapController @pytest.fixture async def sim_panda(): + class Panda(CommonPandaBlocks): + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + super().__init__(name) + + async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim, timeout) + async with DeviceCollector(sim=True): - sim_panda = PandA("PANDACONTROLLER:", "sim_panda") + sim_panda = Panda("PANDACONTROLLER:", "sim_panda") + sim_panda.phase_1_signal_units = epics_signal_rw(int, "") + assert sim_panda.name == "sim_panda" yield sim_panda +async def test_panda_controller_not_filled_blocks(): + class PcapBlock(Device): + pass # Not filled + + pandaController = PandaPcapController(pcap=PcapBlock()) + with patch("ophyd_async.panda._panda_controller.wait_for_value", return_value=None): + with pytest.raises(AttributeError) as exc: + await pandaController.arm(num=1, trigger=DetectorTrigger.constant_gate) + assert ("'PcapBlock' object has no attribute 'arm'") in str(exc.value) + + async def test_panda_controller_arm_disarm(sim_panda): - pandaController = PandaPcapController(pcap=sim_panda.pcap) - with patch("ophyd_async.panda.panda_controller.wait_for_value", return_value=None): + pandaController = PandaPcapController(sim_panda.pcap) + with patch("ophyd_async.panda._panda_controller.wait_for_value", return_value=None): await pandaController.arm(num=1, trigger=DetectorTrigger.constant_gate) await pandaController.disarm() -async def test_panda_controller_wrong_trigger(sim_panda): - pandaController = PandaPcapController(pcap=sim_panda.pcap) +async def test_panda_controller_wrong_trigger(): + pandaController = PandaPcapController(None) with pytest.raises(AssertionError): await pandaController.arm(num=1, trigger=DetectorTrigger.internal) diff --git a/tests/panda/test_panda_utils.py b/tests/panda/test_panda_utils.py index c0b67a40f7..35e53a07b3 100644 --- a/tests/panda/test_panda_utils.py +++ b/tests/panda/test_panda_utils.py @@ -5,15 +5,29 @@ from ophyd_async.core import save_device from ophyd_async.core.device import DeviceCollector +from ophyd_async.core.utils import DEFAULT_TIMEOUT +from ophyd_async.epics.pvi import fill_pvi_entries from ophyd_async.epics.signal import epics_signal_rw -from ophyd_async.panda import PandA -from ophyd_async.panda.utils import phase_sorter +from ophyd_async.panda import CommonPandaBlocks, TimeUnits +from ophyd_async.panda._common_blocks import DataBlock +from ophyd_async.panda._utils import phase_sorter @pytest.fixture async def sim_panda(): + class Panda(CommonPandaBlocks): + data: DataBlock + + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + super().__init__(name) + + async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim, timeout) + async with DeviceCollector(sim=True): - sim_panda = PandA("PANDA:") + sim_panda = Panda("PANDA") sim_panda.phase_1_signal_units = epics_signal_rw(int, "") assert sim_panda.name == "sim_panda" yield sim_panda @@ -27,8 +41,8 @@ async def test_save_panda(mock_save_to_yaml, sim_panda, RE: RunEngine): [ { "phase_1_signal_units": 0, - "seq.1.prescale_units": "min", - "seq.2.prescale_units": "min", + "seq.1.prescale_units": TimeUnits("min"), + "seq.2.prescale_units": TimeUnits("min"), }, { "data.capture": False, diff --git a/tests/panda/test_table.py b/tests/panda/test_table.py index 0c6f347aa9..735fa32886 100644 --- a/tests/panda/test_table.py +++ b/tests/panda/test_table.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from ophyd_async.panda.table import seq_table_from_arrays +from ophyd_async.panda._table import seq_table_from_arrays def test_from_arrays_inconsistent_lengths(): diff --git a/tests/panda/test_trigger.py b/tests/panda/test_trigger.py index a4c3dc8a78..ac23aeba7c 100644 --- a/tests/panda/test_trigger.py +++ b/tests/panda/test_trigger.py @@ -1,20 +1,30 @@ import pytest -from ophyd_async.core.device import DeviceCollector -from ophyd_async.panda import PandA -from ophyd_async.panda.trigger import StaticSeqTableTriggerLogic +from ophyd_async.core.device import DEFAULT_TIMEOUT, DeviceCollector +from ophyd_async.epics.pvi.pvi import fill_pvi_entries +from ophyd_async.panda import CommonPandaBlocks +from ophyd_async.panda._trigger import StaticSeqTableTriggerLogic @pytest.fixture async def panda(): + class Panda(CommonPandaBlocks): + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + super().__init__(name) + + async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim, timeout) + async with DeviceCollector(sim=True): - sim_panda = PandA("PANDAQSRV:", "sim_panda") + sim_panda = Panda("PANDAQSRV:", "sim_panda") assert sim_panda.name == "sim_panda" yield sim_panda -def test_trigger_logic_has_given_methods(panda: PandA): +def test_trigger_logic_has_given_methods(panda): trigger_logic = StaticSeqTableTriggerLogic(panda.seq[1]) assert hasattr(trigger_logic, "prepare") assert hasattr(trigger_logic, "kickoff") diff --git a/tests/panda/test_writer.py b/tests/panda/test_writer.py index ffb50dff4e..29687a3b74 100644 --- a/tests/panda/test_writer.py +++ b/tests/panda/test_writer.py @@ -4,6 +4,7 @@ import pytest from ophyd_async.core import ( + DEFAULT_TIMEOUT, Device, DeviceCollector, SignalR, @@ -11,44 +12,51 @@ StaticDirectoryProvider, set_sim_value, ) -from ophyd_async.epics.signal.signal import SignalRW -from ophyd_async.panda import PandA -from ophyd_async.panda.writers import PandaHDFWriter -from ophyd_async.panda.writers.hdf_writer import ( +from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries +from ophyd_async.panda import CommonPandaBlocks +from ophyd_async.panda.writers._hdf_writer import ( Capture, CaptureSignalWrapper, + PandaHDFWriter, get_capture_signals, get_signals_marked_for_capture, ) -from ophyd_async.panda.writers.panda_hdf_file import _HDFFile +from ophyd_async.panda.writers._panda_hdf_file import _HDFFile @pytest.fixture -async def sim_panda() -> PandA: - async with DeviceCollector(sim=True): - sim_panda = PandA("SIM_PANDA:", name="sim_panda") - sim_panda.block1 = Device("BLOCK1") # type: ignore[attr-defined] - sim_panda.block2 = Device("BLOCK2") # type: ignore[attr-defined] - sim_panda.block1.test_capture = SignalRW( # type: ignore[attr-defined] - backend=SimSignalBackend(str) - ) - sim_panda.block2.test_capture = SignalRW( # type: ignore[attr-defined] - backend=SimSignalBackend(str) - ) +async def panda_t(): + class CaptureBlock(Device): + test_capture: SignalR - await asyncio.gather( - sim_panda.block1.connect(sim=True), # type: ignore[attr-defined] - sim_panda.block2.connect(sim=True), # type: ignore[attr-defined] - sim_panda.connect(sim=True), - ) + class Panda(CommonPandaBlocks): + block_a: CaptureBlock + block_b: CaptureBlock + + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + create_children_from_annotations(self) + super().__init__(name) + + async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim, timeout) + + yield Panda + + +@pytest.fixture +async def sim_panda(panda_t): + async with DeviceCollector(sim=True): + sim_panda = panda_t("SIM_PANDA", name="sim_panda") set_sim_value( - sim_panda.block1.test_capture, + sim_panda.block_a.test_capture, Capture.MinMaxMean, # type: ignore[attr-defined] ) set_sim_value( - sim_panda.block2.test_capture, + sim_panda.block_b.test_capture, Capture.No, # type: ignore[attr-defined] ) @@ -57,10 +65,15 @@ async def sim_panda() -> PandA: @pytest.fixture async def sim_writer(tmp_path, sim_panda) -> PandaHDFWriter: - dir_prov = StaticDirectoryProvider(str(tmp_path), "", "/data.h5") + dir_prov = StaticDirectoryProvider( + directory_path=str(tmp_path), filename_prefix="", filename_suffix="/data.h5" + ) async with DeviceCollector(sim=True): writer = PandaHDFWriter( - "TEST-PANDA", dir_prov, lambda: "test-panda", panda_device=sim_panda + prefix="TEST-PANDA", + directory_provider=dir_prov, + name_provider=lambda: "test-panda", + panda_device=sim_panda, ) return writer @@ -78,8 +91,8 @@ async def test_get_capture_signals_gets_all_signals(sim_panda): ) capture_signals = get_capture_signals(sim_panda) expected_signals = [ - "block1.test_capture", - "block2.test_capture", + "block_a.test_capture", + "block_b.test_capture", "test_seq.seq1_capture", "test_seq.seq2_capture", ] @@ -89,18 +102,18 @@ async def test_get_capture_signals_gets_all_signals(sim_panda): async def test_get_signals_marked_for_capture(sim_panda): capture_signals = { - "block1.test_capture": sim_panda.block1.test_capture, - "block2.test_capture": sim_panda.block2.test_capture, + "block_a.test_capture": sim_panda.block_a.test_capture, + "block_b.test_capture": sim_panda.block_b.test_capture, } signals_marked_for_capture = await get_signals_marked_for_capture(capture_signals) assert len(signals_marked_for_capture) == 1 - assert signals_marked_for_capture["block1.test"].capture_type == Capture.MinMaxMean + assert signals_marked_for_capture["block_a.test"].capture_type == Capture.MinMaxMean async def test_open_returns_correct_descriptors(sim_writer: PandaHDFWriter): assert hasattr(sim_writer.panda_device, "data") - cap1 = sim_writer.panda_device.block1.test_capture # type: ignore[attr-defined] - cap2 = sim_writer.panda_device.block2.test_capture # type: ignore[attr-defined] + cap1 = sim_writer.panda_device.block_a.test_capture # type: ignore[attr-defined] + cap2 = sim_writer.panda_device.block_b.test_capture # type: ignore[attr-defined] set_sim_value(cap1, Capture.MinMaxMean) set_sim_value(cap2, Capture.Value) description = await sim_writer.open() # to make capturing status not time out @@ -112,10 +125,10 @@ async def test_open_returns_correct_descriptors(sim_writer: PandaHDFWriter): assert "source" in entry assert entry.get("external") == "STREAM:" expected_datakeys = [ - "test-panda-block1-test-Min", - "test-panda-block1-test-Max", - "test-panda-block1-test-Mean", - "test-panda-block2-test-Value", + "test-panda-block_a-test-Min", + "test-panda-block_a-test-Max", + "test-panda-block_a-test-Mean", + "test-panda-block_b-test-Value", ] for key in expected_datakeys: assert key in description @@ -159,8 +172,8 @@ async def test_wait_for_index(sim_writer: PandaHDFWriter): async def test_collect_stream_docs(sim_writer: PandaHDFWriter): # Give the sim writer datasets - cap1 = sim_writer.panda_device.block1.test_capture # type: ignore[attr-defined] - cap2 = sim_writer.panda_device.block2.test_capture # type: ignore[attr-defined] + cap1 = sim_writer.panda_device.block_a.test_capture # type: ignore[attr-defined] + cap2 = sim_writer.panda_device.block_b.test_capture # type: ignore[attr-defined] set_sim_value(cap1, Capture.MinMaxMean) set_sim_value(cap2, Capture.Value) await sim_writer.open() @@ -169,7 +182,7 @@ async def test_collect_stream_docs(sim_writer: PandaHDFWriter): assert type(sim_writer._file) is _HDFFile assert sim_writer._file._last_emitted == 1 resource_doc = sim_writer._file._bundles[0].stream_resource_doc - assert resource_doc["data_key"] == "test-panda-block1-test-Min" + assert resource_doc["data_key"] == "test-panda-block_a-test-Min" assert "sim_panda/data.h5" in resource_doc["resource_path"] @@ -183,7 +196,7 @@ async def get_numeric_signal(_): } with patch( - "ophyd_async.panda.writers.hdf_writer.get_signals_marked_for_capture", + "ophyd_async.panda.writers._hdf_writer.get_signals_marked_for_capture", get_numeric_signal, ): assert "test-panda-block-1-Capture.Value" in await sim_writer.open() diff --git a/tests/test_flyer_with_panda.py b/tests/test_flyer_with_panda.py index d0cfab8221..521f3f1bbc 100644 --- a/tests/test_flyer_with_panda.py +++ b/tests/test_flyer_with_panda.py @@ -19,8 +19,9 @@ from ophyd_async.core.detector import StandardDetector from ophyd_async.core.device import DeviceCollector from ophyd_async.core.signal import observe_value, set_sim_value -from ophyd_async.panda import PandA -from ophyd_async.panda.trigger import StaticSeqTableTriggerLogic +from ophyd_async.epics.pvi.pvi import fill_pvi_entries +from ophyd_async.panda import CommonPandaBlocks +from ophyd_async.panda._trigger import StaticSeqTableTriggerLogic from ophyd_async.planstubs import ( prepare_static_seq_table_flyer_and_detectors_with_same_trigger, ) @@ -115,8 +116,17 @@ async def dummy_arm_2(self=None, trigger=None, num=0, exposure=None): @pytest.fixture async def panda(): + class Panda(CommonPandaBlocks): + def __init__(self, prefix: str, name: str = ""): + self._prefix = prefix + super().__init__(name) + + async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT): + await fill_pvi_entries(self, self._prefix + "PVI", timeout=timeout, sim=sim) + await super().connect(sim, timeout) + async with DeviceCollector(sim=True): - sim_panda = PandA("PANDAQSRV:", "sim_panda") + sim_panda = Panda("PANDAQSRV:", "sim_panda") assert sim_panda.name == "sim_panda" yield sim_panda