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
4 changes: 4 additions & 0 deletions src/fastcs_eiger/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from fastcs_eiger import __version__
from fastcs_eiger.controllers.eiger_controller import EigerController
from fastcs_eiger.controllers.odin.eiger_odin_controller import EigerOdinController
from fastcs_eiger.eiger_parameter import EigerAPIVersion

__all__ = ["main"]

Expand Down Expand Up @@ -47,6 +48,7 @@ def ioc(
pv_prefix: str = typer.Argument(),
ip: str = typer.Option("127.0.0.1", help="IP address of Eiger detector"),
port: int = typer.Option(8081, help="Port of Eiger HTTP server"),
api_version: EigerAPIVersion = typer.Option("1.8.0", help="Version of Eiger API"), # noqa: B008
odin_ip: str | None = typer.Option(None, help="IP address of odin control server"),
odin_port: int = typer.Option(8888, help="Port of odin control server"),
log_level: LogLevel = LogLevel.TRACE,
Expand All @@ -58,11 +60,13 @@ def ioc(
if odin_ip is None:
controller = EigerController(
connection_settings=IPConnectionSettings(ip=ip, port=port),
api_version=api_version,
)
else:
controller = EigerOdinController(
detector_connection_settings=IPConnectionSettings(ip=ip, port=port),
odin_connection_settings=IPConnectionSettings(ip=odin_ip, port=odin_port),
api_version=api_version,
)

transports = [
Expand Down
14 changes: 10 additions & 4 deletions src/fastcs_eiger/controllers/eiger_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from fastcs_eiger.controllers.eiger_monitor_controller import EigerMonitorController
from fastcs_eiger.controllers.eiger_stream_controller import EigerStreamController
from fastcs_eiger.controllers.eiger_subsystem_controller import EigerSubsystemController
from fastcs_eiger.eiger_parameter import EIGER_PARAMETER_SUBSYSTEMS
from fastcs_eiger.eiger_parameter import EIGER_PARAMETER_SUBSYSTEMS, EigerAPIVersion
from fastcs_eiger.http_connection import HTTPConnection, HTTPRequestError


Expand All @@ -27,15 +27,17 @@ class EigerController(Controller):
# Internal Attribute
stale_parameters = AttrR(Bool())

def __init__(self, connection_settings: IPConnectionSettings) -> None:
def __init__(
self, connection_settings: IPConnectionSettings, api_version: EigerAPIVersion
) -> None:
super().__init__()
self.connection_settings = connection_settings

self.logger = bind_logger(__class__.__name__)

self.connection = HTTPConnection(connection_settings)
self._parameter_update_lock = asyncio.Lock()
self.queue = asyncio.Queue()
self._api_version: EigerAPIVersion = api_version

async def initialise(self) -> None:
"""Create attributes by introspecting detector.
Expand All @@ -52,12 +54,13 @@ async def initialise(self) -> None:
controller = EigerDetectorController(
self.connection,
self.queue_subsystem_update,
self._api_version,
)
# detector subsystem initialises first
# Check current state of detector_state to see
# if initializing is required.
state_val = await self.connection.get(
"detector/api/1.8.0/status/state"
f"detector/api/{self._api_version}/status/state"
)
if state_val["value"] == "na":
print("Initializing Detector")
Expand All @@ -67,16 +70,19 @@ async def initialise(self) -> None:
controller = EigerMonitorController(
self.connection,
self.queue_subsystem_update,
self._api_version,
)
case "stream":
controller = EigerStreamController(
self.connection,
self.queue_subsystem_update,
self._api_version,
)
case _:
raise NotImplementedError(
f"No subcontroller implemented for subsystem {subsystem}"
)

self.add_sub_controller(subsystem.capitalize(), controller)
await controller.initialise()

Expand Down
21 changes: 12 additions & 9 deletions src/fastcs_eiger/controllers/eiger_detector_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
from fastcs.methods import command

from fastcs_eiger.controllers.eiger_subsystem_controller import EigerSubsystemController
from fastcs_eiger.eiger_parameter import EigerAPIVersion


def command_uri(key: str) -> str:
return f"detector/api/1.8.0/command/{key}"
def command_uri(api_version: EigerAPIVersion, key: str) -> str:
return f"detector/api/{api_version}/command/{key}"


def detector_command(fn) -> Any:
Expand All @@ -25,30 +26,32 @@ class EigerDetectorController(EigerSubsystemController):

@detector_command
async def initialize(self):
await self.connection.put(command_uri("initialize"))
await self.connection.put(command_uri(self._api_version, key="initialize"))

@detector_command
async def arm(self):
await self.connection.put(command_uri("arm"))
await self.connection.put(command_uri(self._api_version, key="arm"))

@detector_command
async def trigger(self):
match self.trigger_mode.get(), self.trigger_exposure.get():
case ("inte", exposure) if exposure > 0.0:
await self.connection.put(command_uri("trigger"), exposure)
await self.connection.put(
command_uri(self._api_version, key="trigger"), exposure
)
case ("ints" | "inte", _):
await self.connection.put(command_uri("trigger"))
await self.connection.put(command_uri(self._api_version, key="trigger"))
case _:
raise RuntimeError("Can only do soft trigger in 'ints' or 'inte' mode")

@detector_command
async def disarm(self):
await self.connection.put(command_uri("disarm"))
await self.connection.put(command_uri(self._api_version, key="disarm"))

@detector_command
async def abort(self):
await self.connection.put(command_uri("abort"))
await self.connection.put(command_uri(self._api_version, key="abort"))

@detector_command
async def cancel(self):
await self.connection.put(command_uri("cancel"))
await self.connection.put(command_uri(self._api_version, key="cancel"))
2 changes: 1 addition & 1 deletion src/fastcs_eiger/controllers/eiger_monitor_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class EigerMonitorController(EigerSubsystemController):
async def handle_monitor(self):
"""Poll monitor images to display."""
response, image_bytes = await self.connection.get_bytes(
"monitor/api/1.8.0/images/next"
f"monitor/api/{self._api_version}/images/next"
)
if response.status != 200:
return
Expand Down
12 changes: 9 additions & 3 deletions src/fastcs_eiger/controllers/eiger_subsystem_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from fastcs_eiger.eiger_parameter import (
EIGER_PARAMETER_MODES,
EigerAPIVersion,
EigerParameterRef,
EigerParameterResponse,
key_to_attribute_name,
Expand Down Expand Up @@ -54,26 +55,30 @@ def __init__(
self,
connection: HTTPConnection,
queue_subsystem_update: Callable[[list[Coroutine]], Coroutine],
api_version: EigerAPIVersion,
):
self.logger = bind_logger(__class__.__name__)

self.connection = connection
self._queue_subsystem_update = queue_subsystem_update
self._io = EigerAttributeIO(connection, self.update_now, self.queue_update)
super().__init__(ios=[self._io])
self._api_version: EigerAPIVersion = api_version

async def _introspect_detector_subsystem(self) -> list[EigerParameterRef]:
parameters = []
for mode in EIGER_PARAMETER_MODES:
subsystem_keys = [
parameter
for parameter in await self.connection.get(
f"{self._subsystem}/api/1.8.0/{mode}/keys"
f"{self._subsystem}/api/{self._api_version}/{mode}/keys"
)
if parameter not in IGNORED_KEYS
] + MISSING_KEYS[self._subsystem][mode]
requests = [
self.connection.get(f"{self._subsystem}/api/1.8.0/{mode}/{key}")
self.connection.get(
f"{self._subsystem}/api/{self._api_version}/{mode}/{key}"
)
for key in subsystem_keys
]
responses = await asyncio.gather(*requests)
Expand All @@ -83,6 +88,7 @@ async def _introspect_detector_subsystem(self) -> list[EigerParameterRef]:
EigerParameterRef(
key=key,
subsystem=self._subsystem,
api_version=self._api_version,
mode=mode,
response=EigerParameterResponse.model_validate(response),
update_period=ONCE if mode == "config" else 0.2,
Expand Down Expand Up @@ -119,7 +125,7 @@ def _create_attributes(cls, parameters: list[EigerParameterRef]):
attributes: dict[str, Attribute] = {}
for parameter in parameters:
group = cls._group(parameter)
match parameter.response.access_mode:
match parameter.access_mode:
case "r":
attributes[parameter.attribute_name] = AttrR(
parameter.fastcs_datatype,
Expand Down
4 changes: 3 additions & 1 deletion src/fastcs_eiger/controllers/odin/eiger_odin_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from fastcs_eiger.controllers.eiger_controller import EigerController
from fastcs_eiger.controllers.odin.odin_controller import OdinController
from fastcs_eiger.eiger_parameter import EigerAPIVersion


class EigerOdinController(EigerController):
Expand All @@ -13,8 +14,9 @@ def __init__(
self,
detector_connection_settings: IPConnectionSettings,
odin_connection_settings: IPConnectionSettings,
api_version: EigerAPIVersion,
) -> None:
super().__init__(detector_connection_settings)
super().__init__(detector_connection_settings, api_version)

self.OD = OdinController(odin_connection_settings)

Expand Down
18 changes: 16 additions & 2 deletions src/fastcs_eiger/eiger_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
from fastcs.datatypes import Bool, DataType, Float, Int, String
from pydantic import BaseModel

EigerAPIVersion = Literal["1.6.0", "1.8.0"]


class EigerParameterResponse(BaseModel):
access_mode: Literal["r", "w", "rw"]
access_mode: Literal["r", "w", "rw"] | None = None
allowed_values: Any | None = None
min: float | int | None = None
value: Any
Expand All @@ -26,6 +28,8 @@ class EigerParameterRef(AttributeIORef):
"""Last section of URI within a subsystem/mode."""
subsystem: Literal["detector", "stream", "monitor"]
"""Subsystem within detector API."""
api_version: EigerAPIVersion = "1.8.0"
"""Version of API to use."""
mode: Literal["status", "config"]
"""Mode of parameter within subsystem."""
response: EigerParameterResponse
Expand All @@ -38,7 +42,7 @@ def attribute_name(self):
@property
def uri(self) -> str:
"""Full URI for HTTP requests."""
return f"{self.subsystem}/api/1.8.0/{self.mode}/{self.key}"
return f"{self.subsystem}/api/{self.api_version}/{self.mode}/{self.key}"

@property
def fastcs_datatype(self) -> DataType:
Expand All @@ -52,6 +56,16 @@ def fastcs_datatype(self) -> DataType:
case "string" | "datetime" | "State" | "string[]":
return String()

@property
def access_mode(self) -> Literal["r", "w", "rw"] | None:
if self.response.access_mode is None:
if self.mode == "status":
return "r"
elif self.mode == "config":
return "rw"
else:
return self.response.access_mode

def __repr__(self):
name = self.__class__.__name__
return f"{name}(subsystem={self.subsystem}, mode={self.mode}, key={self.key})"
Expand Down
4 changes: 3 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ def sim_eiger(request):

@pytest.fixture
def mock_connection(mocker: MockerFixture):
eiger_controller = EigerController(IPConnectionSettings("127.0.0.1", 80))
eiger_controller = EigerController(
IPConnectionSettings("127.0.0.1", 80), api_version="1.8.0"
)
connection = mocker.patch.object(eiger_controller, "connection")
connection.get = mock.AsyncMock()
connection.put = mock.AsyncMock()
Expand Down
24 changes: 18 additions & 6 deletions tests/system/test_eiger_introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ def _serialise_parameter(parameter: EigerParameterRef) -> dict:
@pytest.mark.asyncio
@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))
controller = EigerController(
IPConnectionSettings("127.0.0.1", 8081), api_version="1.8.0"
)
await controller.initialise()
serialised_parameters: dict[str, dict[str, Any]] = {}
subsystem_parameters = {}
Expand Down Expand Up @@ -95,7 +97,9 @@ async def test_attribute_creation(sim_eiger):
@pytest.mark.asyncio
@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))
controller = EigerController(
IPConnectionSettings("127.0.0.1", 8081), api_version="1.8.0"
)
await controller.initialise()

for subsystem in MISSING_KEYS:
Expand Down Expand Up @@ -126,7 +130,9 @@ async def test_controller_groups_and_parameters(sim_eiger):
async def test_threshold_mode_api_inconsistency_handled(
sim_eiger, mocker: MockerFixture
):
controller = EigerController(IPConnectionSettings("127.0.0.1", 8081))
controller = EigerController(
IPConnectionSettings("127.0.0.1", 8081), api_version="1.8.0"
)
await controller.initialise()

detector_controller = controller.sub_controllers["Detector"]
Expand Down Expand Up @@ -157,7 +163,9 @@ async def test_threshold_mode_api_inconsistency_handled(
async def test_fetch_before_returning_parameters(sim_eiger, mocker: MockerFixture):
# Need to mock @scan to spy controller.update()
with patch("fastcs_eiger.controllers.eiger_controller.scan"):
controller = EigerController(IPConnectionSettings("127.0.0.1", 8081))
controller = EigerController(
IPConnectionSettings("127.0.0.1", 8081), api_version="1.8.0"
)
await controller.initialise()

detector_controller = controller.sub_controllers["Detector"]
Expand Down Expand Up @@ -209,7 +217,9 @@ async def test_fetch_before_returning_parameters(sim_eiger, mocker: MockerFixtur
async def test_stale_propagates_to_top_controller(
sim_eiger,
):
controller = EigerController(IPConnectionSettings("127.0.0.1", 8081))
controller = EigerController(
IPConnectionSettings("127.0.0.1", 8081), api_version="1.8.0"
)
await controller.initialise()

detector_controller = controller.sub_controllers["Detector"]
Expand Down Expand Up @@ -268,7 +278,9 @@ async def test_attribute_validation_accepts_valid_types(mock_connection, valid_t
async def test_eiger_controller_trigger_correctly_introspected(
mocker: MockerFixture, sim_eiger
):
controller = EigerController(IPConnectionSettings("127.0.0.1", 8081))
controller = EigerController(
IPConnectionSettings("127.0.0.1", 8081), api_version="1.8.0"
)
await controller.initialise()

detector_controller = controller.sub_controllers["Detector"]
Expand Down
Loading
Loading