Skip to content
Open
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: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ classifiers = [
description = "Eiger control system integration with FastCS"
dependencies = [
"aiohttp",
"fastcs[epicsca]~=0.11.3",
"fastcs-odin @ git+https://github.com/DiamondLightSource/fastcs-odin.git@0.7.0",
"fastcs[epicsca]",
"fastcs-odin @ git+https://github.com/DiamondLightSource/fastcs-odin.git@0.8.0a1",
"numpy",
"pillow",
"typer",
Expand Down
27 changes: 6 additions & 21 deletions run_acquisition.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,44 +31,29 @@ async def run_acquisition(

print("Configuring")
await asyncio.gather(
caput(f"{odin_prefix}:EF:BlockSize", 1),
caput_str(f"{odin_prefix}:EF:Acqid", file_name),
caput_str(f"{odin_prefix}:FP:FilePath", file_path),
caput_str(f"{odin_prefix}:FP:FilePrefix", file_name),
caput_str(f"{odin_prefix}:FP:AcquisitionId", file_name),
caput_str(f"{odin_prefix}:MW:Directory", file_path),
caput_str(f"{odin_prefix}:MW:FilePrefix", file_name),
caput_str(f"{odin_prefix}:MW:AcquisitionId", file_name),
caput(f"{odin_prefix}:BlockSize", 1),
caput_str(f"{odin_prefix}:FilePath", file_path),
caput_str(f"{odin_prefix}:AcquisitionId", file_name),
caput(f"{odin_prefix}:FP:Frames", frames),
caput_str(f"{odin_prefix}:FP:DataCompression", "BSLZ4"),
caput(f"{eiger_prefix}:Detector:Nimages", frames),
caput(f"{eiger_prefix}:Detector:Ntrigger", 1),
caput(f"{eiger_prefix}:Detector:FrameTime", exposure_time),
# caput(f"{eiger_prefix}:Detector:TriggerMode", "ints"), # for real detector
caput_str(f"{eiger_prefix}:Detector:TriggerMode", "ints"), # for tickit sim
)
await pv_equals(f"{eiger_prefix}:StaleParameters", 0)

print("Arming")
await caput(f"{eiger_prefix}:Detector:Arm", True)

datatype = f"uint{await aioca.caget(f'{eiger_prefix}:Detector:BitDepthImage')}"
await caput_str(f"{odin_prefix}:FP:DataDatatype", datatype)
await caput(f"{eiger_prefix}:ArmWhenReady", True)

print("Starting writing")
await caput(f"{odin_prefix}:FP:StartWriting", True)
await asyncio.sleep(1)
await asyncio.gather(
pv_equals(f"{odin_prefix}:FP:Writing", 1, timeout=5),
pv_equals(f"{odin_prefix}:EF:Ready", 1, timeout=5),
)
await caput(f"{eiger_prefix}:StartWriting", True)

print("Triggering")
await caput(f"{eiger_prefix}:Detector:Trigger", True, wait=False)

print("Waiting")
await pv_equals(
f"{odin_prefix}:FP:Writing",
f"{odin_prefix}:Writing",
0,
timeout=exposure_time * frames * 5, # tickit sim is much slower than requested
)
Expand Down
4 changes: 3 additions & 1 deletion src/fastcs_eiger/__main__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from pathlib import Path
from typing import Optional

import softioc.pvlog # noqa: F401
import typer
from fastcs.connections import IPConnectionSettings
from fastcs.launch import FastCS
from fastcs.logging import LogLevel, configure_logging
from fastcs.logging import LogLevel, configure_logging, intercept_std_logger
from fastcs.transports.epics import EpicsGUIOptions, EpicsIOCOptions
from fastcs.transports.epics.ca.transport import EpicsCATransport

Expand Down Expand Up @@ -56,6 +57,7 @@ def ioc(
ui_path = OPI_PATH if OPI_PATH.is_dir() else Path.cwd() / "opi"

configure_logging(log_level)
intercept_std_logger("root")

if odin_ip is None:
controller = EigerController(
Expand Down
37 changes: 31 additions & 6 deletions src/fastcs_eiger/controllers/eiger_controller.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import asyncio
from collections.abc import Coroutine

from fastcs.attributes import AttrR
from fastcs.attributes import AttrR, AttrRW
from fastcs.connections import IPConnectionSettings
from fastcs.controllers import Controller
from fastcs.datatypes import Bool
from fastcs.datatypes import Bool, Int
from fastcs.logging import bind_logger
from fastcs.methods import scan
from fastcs.methods import command, scan

from fastcs_eiger.controllers.eiger_detector_controller import EigerDetectorController
from fastcs_eiger.controllers.eiger_monitor_controller import EigerMonitorController
Expand All @@ -15,6 +15,8 @@
from fastcs_eiger.eiger_parameter import EIGER_PARAMETER_SUBSYSTEMS, EigerAPIVersion
from fastcs_eiger.http_connection import HTTPConnection, HTTPRequestError

COMMAND_GROUP = "Command"


class EigerController(Controller):
"""Root controller for Eiger detectors
Expand All @@ -24,8 +26,16 @@ class EigerController(Controller):
port: Port of Eiger detector
"""

# Internal Attribute
detector: EigerDetectorController

# Internal Attributes
stale_parameters = AttrR(Bool())
arm_timeout = AttrRW(
Int(min=1),
initial_value=3,
description="Timeout for arm command",
group=COMMAND_GROUP,
)

def __init__(
self, connection_settings: IPConnectionSettings, api_version: EigerAPIVersion
Expand Down Expand Up @@ -82,8 +92,7 @@ async def initialise(self) -> None:
raise NotImplementedError(
f"No subcontroller implemented for subsystem {subsystem}"
)

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

except HTTPRequestError:
Expand Down Expand Up @@ -120,3 +129,19 @@ async def queue_subsystem_update(self, coros: list[Coroutine]):
async with self._parameter_update_lock:
for coro in coros:
await self.queue.put(coro)

@command(group=COMMAND_GROUP)
async def arm_when_ready(self):
"""Arm detector and return when ready to send triggers

Wait for parmeters to be synchronised before arming detector

Raises:
TimeoutError: If parameters are not synchronised or arm PUT request fails

"""
await self.stale_parameters.wait_for_value(
False, timeout=self.arm_timeout.get()
)

await self.detector.arm()
5 changes: 4 additions & 1 deletion src/fastcs_eiger/controllers/eiger_detector_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ class EigerDetectorController(EigerSubsystemController):

# Internal attribute to control triggers in `inte` mode
trigger_exposure = AttrRW(Float())
# Introspected attribute needed for trigger logic

# Introspected attributes needed for internal logic
bit_depth_image: AttrR[int]
compression: AttrRW[str]
trigger_mode: AttrR[str]

@detector_command
Expand Down
7 changes: 5 additions & 2 deletions src/fastcs_eiger/controllers/odin/eiger_fan.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastcs.attributes import AttrR
from fastcs.attributes import AttrR, AttrRW
from fastcs.datatypes import Bool
from fastcs_odin.controllers import OdinSubController
from fastcs_odin.io import StatusSummaryAttributeIORef
Expand All @@ -9,6 +9,9 @@ class EigerFanAdapterController(OdinSubController):
"""Controller for an EigerFan adapter in an odin control server"""

state: AttrR[str]
acqid: AttrRW[str]
block_size: AttrRW[int]
ready: AttrR[bool]

async def initialise(self):
for parameter in self.parameters:
Expand All @@ -22,7 +25,7 @@ async def initialise(self):
)

# Manually validate `state` to get a nicer error message if not introspected
self._validate_hinted_attributes()
self._validate_hinted_attribute("state")

self.ready = AttrR(
Bool(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from fastcs.attributes import AttrRW
from fastcs_odin.controllers.odin_data.frame_processor import (
FrameProcessorAdapterController,
)


class EigerFrameProcessorAdapterController(FrameProcessorAdapterController):
data_compression: AttrRW[str]
data_datatype: AttrRW[str]
49 changes: 48 additions & 1 deletion src/fastcs_eiger/controllers/odin/eiger_odin_controller.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import asyncio

from fastcs.attributes import AttrRW
from fastcs.connections import IPConnectionSettings
from fastcs.datatypes import Int
from fastcs.methods import command

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


class EigerOdinController(EigerController):
"""Eiger controller with Odin sub controller"""

start_writing_timeout = AttrRW(
Int(min=1),
initial_value=5,
description="Timeout for start writing command",
group=COMMAND_GROUP,
)

def __init__(
self,
detector_connection_settings: IPConnectionSettings,
Expand All @@ -24,3 +34,40 @@ async def initialise(self) -> None:
"""Initialise eiger controller and odin controller"""

await asyncio.gather(super().initialise(), self.OD.initialise())

@command(group=COMMAND_GROUP)
async def arm_when_ready(self):
"""Check eiger fan is ready before reporting arm as successful

Raises:
TimeoutError: If eiger fan is not ready

"""
await super().arm_when_ready()

try:
await self.OD.EF.ready.wait_for_value(True, timeout=self.arm_timeout.get())
except TimeoutError as e:
raise TimeoutError("Eiger fan not ready") from e

@command(group=COMMAND_GROUP)
async def start_writing(self):
"""Sync eiger parameters to file writers, start writing and return when ready

Raises:
TimeoutError: If file writers fail to start

"""
await asyncio.gather(
self.OD.FP.data_compression.put(self.detector.compression.get().upper()),
self.OD.FP.data_datatype.put(f"uint{self.detector.bit_depth_image.get()}"),
)

await self.OD.FP.start_writing()

try:
await self.OD.writing.wait_for_value(
True, timeout=self.start_writing_timeout.get()
)
except TimeoutError as e:
raise TimeoutError("File writers failed to start") from e
48 changes: 45 additions & 3 deletions src/fastcs_eiger/controllers/odin/odin_controller.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,59 @@
from fastcs.attributes import AttrR
from fastcs.attributes import AttrR, AttrRW
from fastcs.controllers import BaseController
from fastcs.datatypes import Bool
from fastcs.datatypes import Bool, Int, String
from fastcs_odin.controllers import OdinController as _OdinController
from fastcs_odin.controllers.odin_data.meta_writer import MetaWriterAdapterController
from fastcs_odin.http_connection import HTTPConnection
from fastcs_odin.io import StatusSummaryAttributeIORef
from fastcs_odin.io.config_fan_sender_attribute_io import ConfigFanAttributeIORef
from fastcs_odin.util import OdinParameter

from fastcs_eiger.controllers.odin.eiger_fan import EigerFanAdapterController
from fastcs_eiger.controllers.odin.eiger_fp_adapter_controller import (
EigerFrameProcessorAdapterController,
)


class OdinController(_OdinController):
"""Eiger-specific Odin controller"""

writing: AttrR = AttrR(
FP: EigerFrameProcessorAdapterController
EF: EigerFanAdapterController
MW: MetaWriterAdapterController

writing = AttrR(
Bool(), io_ref=StatusSummaryAttributeIORef([("MW", "FP")], "writing", any)
)

async def initialise(self):
await super().initialise()

self.file_path = AttrRW(
String(),
io_ref=ConfigFanAttributeIORef([self.FP.file_path, self.MW.directory]),
)
self.file_prefix = AttrRW(
String(),
io_ref=ConfigFanAttributeIORef([self.FP.file_prefix, self.MW.file_prefix]),
)
self.acquisition_id = AttrRW(
String(),
io_ref=ConfigFanAttributeIORef(
[
self.file_prefix,
self.FP.acquisition_id,
self.MW.acquisition_id,
self.EF.acqid,
]
),
)
self.block_size = AttrRW(
Int(),
io_ref=ConfigFanAttributeIORef(
[self.FP.process_frames_per_block, self.EF.block_size]
),
)

def _create_adapter_controller(
self,
connection: HTTPConnection,
Expand All @@ -26,6 +64,10 @@ def _create_adapter_controller(
"""Create Eiger-specific adapter controllers."""

match module:
case "FrameProcessorAdapter":
return EigerFrameProcessorAdapterController(
connection, parameters, f"{self.API_PREFIX}/{adapter}", self._ios
)
case "EigerFanAdapter":
return EigerFanAdapterController(
connection, parameters, f"{self.API_PREFIX}/{adapter}", self._ios
Expand Down
12 changes: 6 additions & 6 deletions tests/system/test_eiger_introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ async def test_controller_groups_and_parameters(sim_eiger):
await controller.initialise()

for subsystem in MISSING_KEYS:
subcontroller = controller.sub_controllers[subsystem.title()]
subcontroller = controller.sub_controllers[subsystem]
assert isinstance(subcontroller, EigerSubsystemController)
parameters = await subcontroller._introspect_detector_subsystem()
if subsystem == "detector":
Expand Down Expand Up @@ -135,7 +135,7 @@ async def test_threshold_mode_api_inconsistency_handled(
)
await controller.initialise()

detector_controller = controller.sub_controllers["Detector"]
detector_controller = controller.sub_controllers["detector"]
assert isinstance(detector_controller, EigerDetectorController)

attr: AttrRW = detector_controller.attributes["threshold_1_energy"] # type: ignore
Expand Down Expand Up @@ -168,7 +168,7 @@ async def test_fetch_before_returning_parameters(sim_eiger, mocker: MockerFixtur
)
await controller.initialise()

detector_controller = controller.sub_controllers["Detector"]
detector_controller = controller.sub_controllers["detector"]
assert isinstance(detector_controller, EigerDetectorController)

count_time_attr: AttrRW[float, EigerParameterRef] = (
Expand Down Expand Up @@ -222,7 +222,7 @@ async def test_stale_propagates_to_top_controller(
)
await controller.initialise()

detector_controller = controller.sub_controllers["Detector"]
detector_controller = controller.sub_controllers["detector"]
assert isinstance(detector_controller, EigerDetectorController)
await detector_controller.queue_update(["threshold_energy"])
assert controller.stale_parameters.get() is True
Expand Down Expand Up @@ -283,7 +283,7 @@ async def test_eiger_controller_trigger_correctly_introspected(
)
await controller.initialise()

detector_controller = controller.sub_controllers["Detector"]
detector_controller = controller.sub_controllers["detector"]
assert isinstance(detector_controller, EigerDetectorController)
detector_controller.connection = mocker.AsyncMock()

Expand Down Expand Up @@ -353,7 +353,7 @@ async def test_if_min_value_provided_then_prec_set_correctly(
):
await eiger_controller.initialise()

test_float_attr = eiger_controller.sub_controllers["Detector"].attributes.get(
test_float_attr = eiger_controller.sub_controllers["detector"].attributes.get(
"test_float_attr"
)

Expand Down
Loading
Loading