diff --git a/pyproject.toml b/pyproject.toml index 544e5e2..e3131a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ classifiers = [ description = "Eiger control system integration with FastCS" dependencies = [ "aiohttp", - "fastcs[epicsca]~=0.11.1", + "fastcs[epicsca]~=0.11.3", "fastcs-odin @ git+https://github.com/DiamondLightSource/fastcs-odin.git", "numpy", "pillow", diff --git a/src/fastcs_eiger/eiger_subsystem_controller.py b/src/fastcs_eiger/eiger_subsystem_controller.py index f946307..d6f5942 100644 --- a/src/fastcs_eiger/eiger_subsystem_controller.py +++ b/src/fastcs_eiger/eiger_subsystem_controller.py @@ -5,6 +5,7 @@ from fastcs.attributes import Attribute, AttrR, AttrRW from fastcs.controllers import Controller from fastcs.logging import bind_logger +from fastcs.util import ONCE from fastcs_eiger.eiger_parameter import ( EIGER_PARAMETER_MODES, @@ -84,6 +85,7 @@ async def _introspect_detector_subsystem(self) -> list[EigerParameterRef]: subsystem=self._subsystem, mode=mode, response=EigerParameterResponse.model_validate(response), + update_period=ONCE if mode == "config" else 0.2, ) for key, response in zip(subsystem_keys, responses, strict=False) ] @@ -162,7 +164,7 @@ def _get_update_coros_for_parameters( attr_name = key_to_attribute_name(parameter) match self.attributes.get(attr_name, None): case AttrR(io_ref=EigerParameterRef()) as attr: - coros.append(self._io.do_update(attr)) # type: ignore + coros.append(self._io.update(attr)) # type: ignore case _ as attr: if parameter not in IGNORED_KEYS: print( diff --git a/src/fastcs_eiger/http_connection.py b/src/fastcs_eiger/http_connection.py index e5bdae8..b11fa8e 100644 --- a/src/fastcs_eiger/http_connection.py +++ b/src/fastcs_eiger/http_connection.py @@ -1,3 +1,5 @@ +from typing import Any + from aiohttp import ClientResponse, ClientSession, ClientTimeout from fastcs.connections import IPConnectionSettings @@ -47,7 +49,7 @@ def get_session(self) -> ClientSession: raise ConnectionRefusedError("Session is not open") - async def get(self, uri) -> dict[str, str]: + async def get(self, uri) -> dict[str, Any]: """Perform HTTP GET request and return response content as JSON. Args: diff --git a/src/fastcs_eiger/io.py b/src/fastcs_eiger/io.py index 30beb4f..b4173a0 100644 --- a/src/fastcs_eiger/io.py +++ b/src/fastcs_eiger/io.py @@ -1,6 +1,5 @@ from collections.abc import Awaitable, Callable, Sequence from dataclasses import dataclass -from typing import Any from fastcs.attributes import AttributeIO, AttrR, AttrW from fastcs.datatypes import DType_T @@ -28,8 +27,6 @@ def __init__( self.queue_update = queue_update self.logger = bind_logger(__class__.__name__) - self.first_poll_complete = False - def _handle_params_to_update( self, parameters: list[str], uri: str ) -> tuple[list[str], list[str]]: @@ -67,28 +64,19 @@ async def send( await self.update_now(update_now) await self.queue_update(update_later) - async def do_update(self, attr: AttrR[Any, EigerParameterRef]) -> None: - try: - response = await self.connection.get(attr.io_ref.uri) - value = response["value"] - if isinstance(value, list) and all( - isinstance(s, str) for s in value - ): # error is a list of strings - value = ", ".join(value) - - self.log_event( - "Query for parameter", - uri=attr.io_ref.uri, - response=response, - topic=attr, - ) + async def update(self, attr: AttrR[DType_T, EigerParameterRef]) -> None: + response = await self.connection.get(attr.io_ref.uri) + value = response["value"] + if isinstance(value, list) and all( + isinstance(s, str) for s in value + ): # error is a list of strings + value = ", ".join(value) - await attr.update(value) - except Exception as e: - print(f"Failed to get {attr.io_ref.uri}:\n{e.__class__.__name__} {e}") + self.log_event( + "Query for parameter", + uri=attr.io_ref.uri, + response=response, + topic=attr, + ) - async def update(self, attr: AttrR[DType_T, EigerParameterRef]) -> None: - if attr.io_ref.mode == "config" and self.first_poll_complete: - return - await self.do_update(attr) - self.first_poll_complete = True + await attr.update(value) diff --git a/tests/conftest.py b/tests/conftest.py index 7518cb3..310c930 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,8 +31,8 @@ def pytest_internalerror(excinfo: pytest.ExceptionInfo[Any]): # Stolen from tickit-devices # https://docs.pytest.org/en/latest/example/parametrize.html#indirect-parametrization -@pytest.fixture -def sim_eiger_controller(request): +@pytest.fixture(scope="session") +def sim_eiger(request): """Subprocess that runs ``tickit all ``.""" config_path: str = request.param proc = subprocess.Popen( @@ -50,7 +50,7 @@ def sim_eiger_controller(request): sleep(3) - yield EigerController(IPConnectionSettings("127.0.0.1", 8081)) + yield proc.send_signal(signal.SIGINT) print(proc.communicate()[0]) diff --git a/tests/system/test_eiger_introspection.py b/tests/system/test_eiger_introspection.py index 1ca4d8e..03737a1 100644 --- a/tests/system/test_eiger_introspection.py +++ b/tests/system/test_eiger_introspection.py @@ -6,6 +6,7 @@ import pytest from fastcs.attributes import Attribute, AttrR, AttrRW +from fastcs.connections import IPConnectionSettings from fastcs.datatypes import Float from pydantic import ValidationError from pytest_mock import MockerFixture @@ -42,11 +43,9 @@ def _serialise_parameter(parameter: EigerParameterRef) -> dict: @pytest.mark.asyncio -@pytest.mark.parametrize( - "sim_eiger_controller", [str(HERE / "eiger.yaml")], indirect=True -) -async def test_attribute_creation(sim_eiger_controller: EigerController): - controller = sim_eiger_controller +@pytest.mark.parametrize("sim_eiger", [str(HERE / "eiger.yaml")], indirect=True) +async def test_attribute_creation(sim_eiger): + controller = EigerController(IPConnectionSettings("127.0.0.1", 8081)) await controller.initialise() serialised_parameters: dict[str, dict[str, Any]] = {} subsystem_parameters = {} @@ -94,11 +93,9 @@ async def test_attribute_creation(sim_eiger_controller: EigerController): @pytest.mark.asyncio -@pytest.mark.parametrize( - "sim_eiger_controller", [str(HERE / "eiger.yaml")], indirect=True -) -async def test_controller_groups_and_parameters(sim_eiger_controller: EigerController): - controller = sim_eiger_controller +@pytest.mark.parametrize("sim_eiger", [str(HERE / "eiger.yaml")], indirect=True) +async def test_controller_groups_and_parameters(sim_eiger): + controller = EigerController(IPConnectionSettings("127.0.0.1", 8081)) await controller.initialise() for subsystem in MISSING_KEYS: @@ -125,13 +122,11 @@ async def test_controller_groups_and_parameters(sim_eiger_controller: EigerContr @pytest.mark.asyncio -@pytest.mark.parametrize( - "sim_eiger_controller", [str(HERE / "eiger.yaml")], indirect=True -) +@pytest.mark.parametrize("sim_eiger", [str(HERE / "eiger.yaml")], indirect=True) async def test_threshold_mode_api_inconsistency_handled( - sim_eiger_controller: EigerController, mocker: MockerFixture + sim_eiger, mocker: MockerFixture ): - controller = sim_eiger_controller + controller = EigerController(IPConnectionSettings("127.0.0.1", 8081)) await controller.initialise() detector_controller = controller.sub_controllers["Detector"] @@ -158,15 +153,11 @@ async def test_threshold_mode_api_inconsistency_handled( @pytest.mark.asyncio -@pytest.mark.parametrize( - "sim_eiger_controller", [str(HERE / "eiger.yaml")], indirect=True -) -async def test_fetch_before_returning_parameters( - sim_eiger_controller: EigerController, mocker: MockerFixture -): +@pytest.mark.parametrize("sim_eiger", [str(HERE / "eiger.yaml")], indirect=True) +async def test_fetch_before_returning_parameters(sim_eiger, mocker: MockerFixture): # Need to mock @scan to spy controller.update() with patch("fastcs_eiger.eiger_controller.scan"): - controller = sim_eiger_controller + controller = EigerController(IPConnectionSettings("127.0.0.1", 8081)) await controller.initialise() detector_controller = controller.sub_controllers["Detector"] @@ -184,7 +175,7 @@ async def test_fetch_before_returning_parameters( queue_update_spy = mocker.spy(detector_controller._io, "queue_update") update_now_spy = mocker.spy(detector_controller._io, "update_now") - io_do_update_spy = mocker.spy(detector_controller._io, "do_update") + io_update_spy = mocker.spy(detector_controller._io, "update") await detector_controller._io.send(count_time_attr, 2.0) # bit_depth_image and bit_depth_readout handled early @@ -201,26 +192,24 @@ async def test_fetch_before_returning_parameters( ] ) - updated = [call.args[0].io_ref.key for call in io_do_update_spy.await_args_list] + updated = [call.args[0].io_ref.key for call in io_update_spy.await_args_list] assert "bit_depth_image" in updated assert "count_time" not in updated # queued updated not updated until controller.update() await controller.update() - updated = [call.args[0].io_ref.key for call in io_do_update_spy.await_args_list] + updated = [call.args[0].io_ref.key for call in io_update_spy.await_args_list] assert "count_time" in updated await controller.connection.close() @pytest.mark.asyncio -@pytest.mark.parametrize( - "sim_eiger_controller", [str(HERE / "eiger.yaml")], indirect=True -) +@pytest.mark.parametrize("sim_eiger", [str(HERE / "eiger.yaml")], indirect=True) async def test_stale_propagates_to_top_controller( - sim_eiger_controller: EigerController, + sim_eiger, ): - controller = sim_eiger_controller + controller = EigerController(IPConnectionSettings("127.0.0.1", 8081)) await controller.initialise() detector_controller = controller.sub_controllers["Detector"] @@ -275,13 +264,11 @@ async def test_attribute_validation_accepts_valid_types(mock_connection, valid_t @pytest.mark.asyncio -@pytest.mark.parametrize( - "sim_eiger_controller", [str(HERE / "eiger.yaml")], indirect=True -) +@pytest.mark.parametrize("sim_eiger", [str(HERE / "eiger.yaml")], indirect=True) async def test_eiger_controller_trigger_correctly_introspected( - mocker: MockerFixture, sim_eiger_controller: EigerController + mocker: MockerFixture, sim_eiger ): - controller = sim_eiger_controller + controller = EigerController(IPConnectionSettings("127.0.0.1", 8081)) await controller.initialise() detector_controller = controller.sub_controllers["Detector"]