Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion src/fastcs_eiger/eiger_subsystem_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
]
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion src/fastcs_eiger/http_connection.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any

from aiohttp import ClientResponse, ClientSession, ClientTimeout
from fastcs.connections import IPConnectionSettings

Expand Down Expand Up @@ -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:
Expand Down
40 changes: 14 additions & 26 deletions src/fastcs_eiger/io.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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]]:
Expand Down Expand Up @@ -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)
6 changes: 3 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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>``."""
config_path: str = request.param
proc = subprocess.Popen(
Expand All @@ -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])
Expand Down
57 changes: 22 additions & 35 deletions tests/system/test_eiger_introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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:
Expand All @@ -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"]
Expand All @@ -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"]
Expand All @@ -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
Expand All @@ -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"]
Expand Down Expand Up @@ -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"]
Expand Down
Loading