diff --git a/src/ophyd_async/epics/eiger/__init__.py b/src/ophyd_async/epics/eiger/__init__.py deleted file mode 100644 index d1d145900a..0000000000 --- a/src/ophyd_async/epics/eiger/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from ._odin_io import Odin, OdinWriter, Writing - -__all__ = ["Odin", "OdinWriter", "Writing"] diff --git a/src/ophyd_async/epics/eiger/_odin_io.py b/src/ophyd_async/epics/eiger/_odin_io.py deleted file mode 100644 index e20d465ea1..0000000000 --- a/src/ophyd_async/epics/eiger/_odin_io.py +++ /dev/null @@ -1,148 +0,0 @@ -import asyncio -from collections.abc import AsyncGenerator, AsyncIterator - -from bluesky.protocols import StreamAsset -from event_model import DataKey # type: ignore - -from ophyd_async.core import ( - DEFAULT_TIMEOUT, - DetectorWriter, - Device, - DeviceVector, - PathProvider, - StrictEnum, - observe_value, - set_and_wait_for_other_value, - set_and_wait_for_value, - wait_for_value, -) -from ophyd_async.epics.core import ( - epics_signal_r, - epics_signal_rw, - epics_signal_rw_rbv, -) - - -class Writing(StrictEnum): - CAPTURE = "Capture" - DONE = "Done" - - -class OdinNode(Device): - def __init__(self, prefix: str, name: str = "") -> None: - self.writing = epics_signal_r(str, f"{prefix}Writing_RBV") - self.frames_dropped = epics_signal_r(int, f"{prefix}FramesDropped_RBV") - self.frames_time_out = epics_signal_r(int, f"{prefix}FramesTimedOut_RBV") - self.error_status = epics_signal_r(str, f"{prefix}FPErrorState_RBV") - self.fp_initialised = epics_signal_r(int, f"{prefix}FPProcessConnected_RBV") - self.fr_initialised = epics_signal_r(int, f"{prefix}FRProcessConnected_RBV") - self.num_captured = epics_signal_r(int, f"{prefix}NumCaptured_RBV") - self.clear_errors = epics_signal_rw(int, f"{prefix}FPClearErrors") - self.error_message = epics_signal_rw(str, f"{prefix}FPErrorMessage_RBV") - - super().__init__(name) - - -class Odin(Device): - def __init__(self, prefix: str, name: str = "") -> None: - self.nodes = DeviceVector( - {i: OdinNode(f"{prefix[:-1]}{i + 1}:") for i in range(4)} - ) - - self.capture = epics_signal_rw(Writing, f"{prefix}Capture") - self.capture_rbv = epics_signal_r(str, prefix + "Capture_RBV") - self.num_captured = epics_signal_r(int, f"{prefix}NumCapture_RBV") - self.num_to_capture = epics_signal_rw_rbv(int, f"{prefix}NumCapture") - - self.start_timeout = epics_signal_rw(str, f"{prefix}StartTimeout") - self.timeout_active_rbv = epics_signal_r(str, f"{prefix}TimeoutActive_RBV") - - self.image_height = epics_signal_rw_rbv(int, f"{prefix}ImageHeight") - self.image_width = epics_signal_rw_rbv(int, f"{prefix}ImageWidth") - - self.num_row_chunks = epics_signal_rw_rbv(int, f"{prefix}NumRowChunks") - self.num_col_chunks = epics_signal_rw_rbv(int, f"{prefix}NumColChunks") - - self.file_path = epics_signal_rw_rbv(str, f"{prefix}FilePath") - self.file_name = epics_signal_rw_rbv(str, f"{prefix}FileName") - - self.num_frames_chunks = epics_signal_rw(int, prefix + "NumFramesChunks") - self.meta_active = epics_signal_r(str, prefix + "META:AcquisitionActive_RBV") - self.meta_writing = epics_signal_r(str, prefix + "META:Writing_RBV") - - self.data_type = epics_signal_rw_rbv(str, f"{prefix}DataType") - - super().__init__(name) - - -class OdinWriter(DetectorWriter): - def __init__( - self, - path_provider: PathProvider, - odin_driver: Odin, - ) -> None: - self._drv = odin_driver - self._path_provider = path_provider - super().__init__() - - async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]: - info = self._path_provider(device_name=name) - self._exposures_per_event = exposures_per_event - - await asyncio.gather( - self._drv.file_path.set(str(info.directory_path)), - self._drv.file_name.set(info.filename), - self._drv.data_type.set( - "UInt16" - ), # TODO: Get from eiger https://github.com/bluesky/ophyd-async/issues/529 - self._drv.num_to_capture.set(0), - ) - - await wait_for_value(self._drv.meta_active, "Active", timeout=DEFAULT_TIMEOUT) - - await set_and_wait_for_other_value( - self._drv.capture, - Writing.CAPTURE, - self._drv.capture_rbv, - "Capturing", - set_timeout=None, - wait_for_set_completion=False, - ) # TODO: Investigate why we do not get a put callback when setting capture pv https://github.com/bluesky/ophyd-async/issues/866 - - await wait_for_value(self._drv.meta_writing, "Writing", timeout=DEFAULT_TIMEOUT) - - return await self._describe() - - async def _describe(self) -> dict[str, DataKey]: - data_shape = await asyncio.gather( - self._drv.image_height.get_value(), self._drv.image_width.get_value() - ) - - return { - "data": DataKey( - source=self._drv.file_name.source, - shape=[self._exposures_per_event, *data_shape], - dtype="array", - # TODO: Use correct type based on eiger https://github.com/bluesky/ophyd-async/issues/529 - dtype_numpy=" AsyncGenerator[int, None]: - async for num_captured in observe_value(self._drv.num_captured, timeout): - yield num_captured // self._exposures_per_event - - async def get_indices_written(self) -> int: - return await self._drv.num_captured.get_value() // self._exposures_per_event - - def collect_stream_docs( - self, name: str, indices_written: int - ) -> AsyncIterator[StreamAsset]: - # TODO: Correctly return stream https://github.com/bluesky/ophyd-async/issues/530 - raise NotImplementedError() - - async def close(self) -> None: - await set_and_wait_for_value(self._drv.capture, Writing.DONE) diff --git a/src/ophyd_async/fastcs/eiger/_eiger.py b/src/ophyd_async/fastcs/eiger/_eiger.py index fd702d13da..23371ee6d6 100644 --- a/src/ophyd_async/fastcs/eiger/_eiger.py +++ b/src/ophyd_async/fastcs/eiger/_eiger.py @@ -1,7 +1,7 @@ from pydantic import Field from ophyd_async.core import AsyncStatus, PathProvider, StandardDetector, TriggerInfo -from ophyd_async.epics.eiger import Odin, OdinWriter +from ophyd_async.fastcs.odin import OdinHdfIO, OdinWriter from ._eiger_controller import EigerController from ._eiger_io import EigerDriverIO @@ -26,7 +26,7 @@ def __init__( name="", ): self.drv = EigerDriverIO(prefix + drv_suffix) - self.odin = Odin(prefix + hdf_suffix) + self.odin = OdinHdfIO(prefix + hdf_suffix) super().__init__( EigerController(self.drv), diff --git a/src/ophyd_async/fastcs/odin/__init__.py b/src/ophyd_async/fastcs/odin/__init__.py index a9a2c5b3bb..6ebb6b5f5b 100644 --- a/src/ophyd_async/fastcs/odin/__init__.py +++ b/src/ophyd_async/fastcs/odin/__init__.py @@ -1 +1,3 @@ -__all__ = [] +from ._odin_io import OdinHdfIO, OdinWriter, OdinWriting + +__all__ = ["OdinHdfIO", "OdinWriter", "OdinWriting"] diff --git a/src/ophyd_async/fastcs/odin/_odin_io.py b/src/ophyd_async/fastcs/odin/_odin_io.py new file mode 100644 index 0000000000..609e033c67 --- /dev/null +++ b/src/ophyd_async/fastcs/odin/_odin_io.py @@ -0,0 +1,102 @@ +import asyncio +from collections.abc import AsyncGenerator, AsyncIterator + +from bluesky.protocols import StreamAsset +from event_model import DataKey + +from ophyd_async.core import ( + DetectorWriter, + Device, + PathProvider, + SignalR, + SignalRW, + StrictEnum, + observe_value, + set_and_wait_for_value, +) +from ophyd_async.fastcs.core import fastcs_connector + + +class OdinWriting(StrictEnum): + ON = "ON" + OFF = "OFF" + + +class OdinHdfIO(Device): + config_hdf_write: SignalRW[OdinWriting] + frames_written: SignalR[int] + frames: SignalRW[int] + data_dims_0: SignalRW[int] + data_dims_1: SignalRW[int] + data_chunks_0: SignalRW[int] + data_chunks_1: SignalRW[int] + data_chunks_2: SignalRW[int] + file_path: SignalRW[str] + file_prefix: SignalRW[str] + data_datatype: SignalRW[str] + + def __init__(self, uri: str, name: str = ""): + super().__init__(name=name, connector=fastcs_connector(self, uri)) + + +class OdinWriter(DetectorWriter): + def __init__( + self, + path_provider: PathProvider, + odin_driver: OdinHdfIO, + ) -> None: + self._drv = odin_driver + self._path_provider = path_provider + super().__init__() + + async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]: + info = self._path_provider(device_name=name) + self._exposures_per_event = exposures_per_event + + await asyncio.gather( + self._drv.file_path.set(str(info.directory_path)), + self._drv.file_prefix.set(info.filename), + self._drv.data_datatype.set( + "UInt16" + ), # TODO: Get from eiger https://github.com/bluesky/ophyd-async/issues/529 + self._drv.frames.set(0), + ) + + await self._drv.config_hdf_write.set(OdinWriting.ON) + + return await self._describe() + + async def _describe(self) -> dict[str, DataKey]: + data_shape = await asyncio.gather( + self._drv.data_dims_0.get_value(), + self._drv.data_dims_1.get_value(), + ) + + return { + "data": DataKey( + source=self._drv.file_prefix.source, + shape=list(data_shape), + dtype="array", + # TODO: Use correct type based on eiger https://github.com/bluesky/ophyd-async/issues/529 + dtype_numpy=" AsyncGenerator[int, None]: + async for num_captured in observe_value(self._drv.frames_written, timeout): + yield num_captured + + async def get_indices_written(self) -> int: + return await self._drv.frames_written.get_value() + + def collect_stream_docs( + self, name: str, indices_written: int + ) -> AsyncIterator[StreamAsset]: + # TODO: Correctly return stream https://github.com/bluesky/ophyd-async/issues/530 + raise NotImplementedError() + + async def close(self) -> None: + await set_and_wait_for_value(self._drv.config_hdf_write, OdinWriting.OFF) diff --git a/tests/epics/eiger/test_odin_io.py b/tests/epics/eiger/test_odin_io.py deleted file mode 100644 index fa3dd587b6..0000000000 --- a/tests/epics/eiger/test_odin_io.py +++ /dev/null @@ -1,106 +0,0 @@ -from pathlib import Path -from unittest.mock import ANY, AsyncMock, MagicMock, call, patch - -import pytest - -from ophyd_async.core import DEFAULT_TIMEOUT, init_devices -from ophyd_async.epics.eiger import Odin, OdinWriter, Writing -from ophyd_async.testing import get_mock_put, set_mock_value - -ODIN_DETECTOR_NAME = "odin_detector" - -OdinDriverAndWriter = tuple[Odin, OdinWriter] - - -@pytest.fixture -def odin_driver_and_writer(RE) -> OdinDriverAndWriter: - with init_devices(mock=True): - driver = Odin("") - writer = OdinWriter(MagicMock(), driver) - - # Set meta and capturing pvs high - set_mock_value(driver.meta_active, "Active") - set_mock_value(driver.capture_rbv, "Capturing") - set_mock_value(driver.meta_writing, "Writing") - return driver, writer - - -async def test_when_open_called_then_file_correctly_set( - odin_driver_and_writer: OdinDriverAndWriter, tmp_path: Path -): - driver, writer = odin_driver_and_writer - path_info = writer._path_provider.return_value # type: ignore - expected_filename = "filename.h5" - path_info.directory_path = tmp_path - path_info.filename = expected_filename - - await writer.open(ODIN_DETECTOR_NAME) - - get_mock_put(driver.file_path).assert_called_once_with(str(tmp_path), wait=ANY) - get_mock_put(driver.file_name).assert_called_once_with(expected_filename, wait=ANY) - - -async def test_when_open_called_then_all_expected_signals_set( - odin_driver_and_writer: OdinDriverAndWriter, -): - driver, writer = odin_driver_and_writer - await writer.open(ODIN_DETECTOR_NAME) - - get_mock_put(driver.data_type).assert_called_once_with("UInt16", wait=ANY) - get_mock_put(driver.num_to_capture).assert_called_once_with(0, wait=ANY) - - get_mock_put(driver.capture).assert_called_once_with(Writing.CAPTURE, wait=ANY) - - -async def test_given_data_shape_set_when_open_called_then_describe_has_correct_shape( - odin_driver_and_writer: OdinDriverAndWriter, -): - driver, writer = odin_driver_and_writer - set_mock_value(driver.image_width, 1024) - set_mock_value(driver.image_height, 768) - description = await writer.open(ODIN_DETECTOR_NAME) - assert description["data"]["shape"] == [1, 768, 1024] - - -async def test_when_closed_then_data_capture_turned_off( - odin_driver_and_writer: OdinDriverAndWriter, -): - driver, writer = odin_driver_and_writer - await writer.close() - get_mock_put(driver.capture).assert_called_once_with(Writing.DONE, wait=ANY) - - -@pytest.mark.asyncio -@patch("ophyd_async.epics.eiger._odin_io.wait_for_value") -@patch("ophyd_async.epics.eiger._odin_io.set_and_wait_for_other_value") -async def test_wait_for_active_before_capture_then_wait_for_writing( - mock_set_and_wait_for_other_value, - mock_wait_for_value, - odin_driver_and_writer, -): - driver, writer = odin_driver_and_writer - - mock_manager = AsyncMock() - mock_manager.attach_mock(mock_wait_for_value, "mock_wait_for_value") - mock_manager.attach_mock( - mock_set_and_wait_for_other_value, "mock_set_and_wait_for_other_value" - ) - - await writer.open(ODIN_DETECTOR_NAME) - - expected_calls = [ - call.mock_wait_for_value(driver.meta_active, "Active", timeout=DEFAULT_TIMEOUT), - call.mock_set_and_wait_for_other_value( - driver.capture, - Writing.CAPTURE, - driver.capture_rbv, - "Capturing", - set_timeout=None, - wait_for_set_completion=False, - ), - call.mock_wait_for_value( - driver.meta_writing, "Writing", timeout=DEFAULT_TIMEOUT - ), - ] - - assert mock_manager.mock_calls == expected_calls diff --git a/tests/fastcs/eiger/test_eiger_detector.py b/tests/fastcs/eiger/test_eiger_detector.py index 0774af4318..d784ade874 100644 --- a/tests/fastcs/eiger/test_eiger_detector.py +++ b/tests/fastcs/eiger/test_eiger_detector.py @@ -4,16 +4,13 @@ from ophyd_async.core import DetectorTrigger, init_devices from ophyd_async.fastcs.eiger import EigerDetector, EigerTriggerInfo -from ophyd_async.testing import get_mock_put, set_mock_value +from ophyd_async.testing import get_mock_put @pytest.fixture def detector(RE): with init_devices(mock=True): detector = EigerDetector("BL03I", MagicMock()) - set_mock_value(detector.odin.meta_active, "Active") - set_mock_value(detector.odin.capture_rbv, "Capturing") - set_mock_value(detector.odin.meta_writing, "Writing") return detector diff --git a/tests/fastcs/odin/test_odin_io.py b/tests/fastcs/odin/test_odin_io.py new file mode 100644 index 0000000000..649a3b8b44 --- /dev/null +++ b/tests/fastcs/odin/test_odin_io.py @@ -0,0 +1,70 @@ +from pathlib import Path +from unittest.mock import ANY, MagicMock + +import pytest + +from ophyd_async.core import init_devices +from ophyd_async.fastcs.odin import OdinHdfIO, OdinWriter, OdinWriting +from ophyd_async.testing import get_mock_put, set_mock_value + +OdinDriverAndWriter = tuple[OdinHdfIO, OdinWriter] + + +@pytest.fixture +def odin_driver_and_writer(RE) -> OdinDriverAndWriter: + with init_devices(mock=True): + driver = OdinHdfIO("") + writer = OdinWriter(MagicMock(), driver) + return driver, writer + + +async def test_when_open_called_then_file_correctly_set( + odin_driver_and_writer: OdinDriverAndWriter, tmp_path: Path +): + driver, writer = odin_driver_and_writer + path_info = writer._path_provider.return_value # type: ignore + expected_filename = "filename.h5" + path_info.directory_path = tmp_path + path_info.filename = expected_filename + + await writer.open("Odin") + + get_mock_put(driver.file_path).assert_called_once_with(str(tmp_path), wait=ANY) + get_mock_put(driver.file_prefix).assert_called_once_with( + expected_filename, wait=ANY + ) + + +async def test_when_open_called_then_all_expected_signals_set( + odin_driver_and_writer: OdinDriverAndWriter, +): + driver, writer = odin_driver_and_writer + await writer.open("Odin") + + get_mock_put(driver.data_datatype).assert_called_once_with("UInt16", wait=ANY) + get_mock_put(driver.frames).assert_called_once_with(0, wait=ANY) + + get_mock_put(driver.config_hdf_write).assert_called_once_with( + OdinWriting.ON, wait=ANY + ) + + +async def test_given_data_shape_set_when_open_called_then_describe_has_correct_shape( + odin_driver_and_writer: OdinDriverAndWriter, +): + driver, writer = odin_driver_and_writer + set_mock_value(driver.data_dims_1, 1024) + set_mock_value(driver.data_dims_0, 768) + description = await writer.open("Odin") + assert description["data"]["shape"] == [768, 1024] + + +async def test_when_closed_then_data_capture_turned_off( + odin_driver_and_writer: OdinDriverAndWriter, +): + driver, writer = odin_driver_and_writer + + await writer.close() + get_mock_put(driver.config_hdf_write).assert_called_once_with( + OdinWriting.OFF, wait=ANY + )