Skip to content

Commit

Permalink
implement review comments
Browse files Browse the repository at this point in the history
  • Loading branch information
jsouter committed Oct 15, 2024
1 parent 7f70499 commit f11fb04
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 90 deletions.
176 changes: 93 additions & 83 deletions src/fastcs_eiger/eiger_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ class EigerHandler:
uri: str
update_period: float = 0.2

async def put(self, controller: "EigerController", _: AttrW, value: Any) -> None:
async def put(
self, controller: "EigerSubsystemController", _: AttrW, value: Any
) -> None:
parameters_to_update = await controller.connection.put(self.uri, value)
if not parameters_to_update:
parameters_to_update = [self.uri.split("/", 4)[-1]]
Expand Down Expand Up @@ -141,6 +143,10 @@ class EigerParameter:
response: dict[str, Any]
"""JSON response from GET of parameter."""

@property
def attribute_name(self):
return _key_to_attribute_name(self.key)

@property
def uri(self) -> str:
"""Full URI for HTTP requests."""
Expand All @@ -163,12 +169,8 @@ class EigerController(Controller):
Sets up all connections with the Simplon API to send and receive information
"""

# Detector parameters to use in internal logic
trigger_mode = AttrRW(String()) # TODO: Include URI and validate type from API

# Internal Attributes
# Internal Attribute
stale_parameters = AttrR(Bool())
trigger_exposure = AttrRW(Float(), handler=LogicHandler())

def __init__(self, ip: str, port: int) -> None:
super().__init__()
Expand All @@ -195,50 +197,29 @@ async def initialise(self) -> None:

try:
for subsystem in EIGER_PARAMETER_SUBSYSTEMS:
if subsystem == "detector":
controller = EigerDetectorController(
self.connection, self._parameter_update_lock
)
else:
controller = EigerSubsystemController(
subsystem, self.connection, self._parameter_update_lock
)
match subsystem:
case "detector":
controller = EigerDetectorController(
self.connection, self._parameter_update_lock
)
case "monitor":
controller = EigerMonitorController(
self.connection, self._parameter_update_lock
)
case "stream":
controller = EigerStreamController(
self.connection, self._parameter_update_lock
)
case _:
raise NotImplementedError(
f"No subcontroller implemented for subsystem {subsystem}"
)
self.register_sub_controller(subsystem.upper(), controller)
await controller.initialise()
except HTTPRequestError:
print("\nAn HTTP request failed while introspecting detector:\n")
raise

@detector_command
async def initialize(self):
await self.connection.put(command_uri("initialize"))

@detector_command
async def arm(self):
await self.connection.put(command_uri("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)
case ("ints" | "inte", _):
await self.connection.put(command_uri("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"))

@detector_command
async def abort(self):
await self.connection.put(command_uri("abort"))

@detector_command
async def cancel(self):
await self.connection.put(command_uri("cancel"))

@scan(0.1)
async def update(self):
"""Periodically check for parameters that need updating from the detector."""
Expand All @@ -248,31 +229,16 @@ async def update(self):
controller_updates = [c.update() for c in self.get_sub_controllers().values()]
await asyncio.gather(*controller_updates)

@scan(1)
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"
)
if response.status != 200:
return
else:
image = Image.open(BytesIO(image_bytes))

# TODO: Populate waveform PV to display as image, once supported in PVI
print(np.array(image))


class EigerSubsystemController(SubController):
_subsystem: Literal["detector", "stream", "monitor"]
stale_parameters = AttrR(Bool())

def __init__(
self,
subsystem: Literal["detector", "stream", "monitor"],
connection: HTTPConnection,
lock: asyncio.Lock,
):
self._subsystem = subsystem
self.connection = connection
self._parameter_update_lock = lock
self._parameter_updates: set[str] = set()
Expand Down Expand Up @@ -316,14 +282,6 @@ async def initialise(self) -> None:
def _group(cls, parameter: EigerParameter):
return f"{parameter.subsystem.capitalize()}{parameter.mode.capitalize()}"

@classmethod
def _attribute_name(self, parameter: EigerParameter):
return _key_to_attribute_name(parameter.key)

@classmethod
def _group_and_name(self, parameter: EigerParameter) -> tuple[str, str]:
return (self._group(parameter), self._attribute_name(parameter))

@classmethod
def _create_attributes(cls, parameters: list[EigerParameter]):
"""Create ``Attribute``s from ``EigerParameter``s.
Expand All @@ -334,7 +292,6 @@ def _create_attributes(cls, parameters: list[EigerParameter]):
"""
attributes: dict[str, Attribute] = {}
for parameter in parameters:
group, attribute_name = cls._group_and_name(parameter)
match parameter.response["value_type"]:
case "float":
datatype = Float()
Expand All @@ -347,15 +304,16 @@ def _create_attributes(cls, parameters: list[EigerParameter]):
case _:
print(f"Failed to handle {parameter}")

group = cls._group(parameter)
match parameter.response["access_mode"]:
case "r":
attributes[attribute_name] = AttrR(
attributes[parameter.attribute_name] = AttrR(
datatype,
handler=EIGER_HANDLERS[parameter.mode](parameter.uri),
group=group,
)
case "rw":
attributes[attribute_name] = AttrRW(
attributes[parameter.attribute_name] = AttrRW(
datatype,
handler=EIGER_HANDLERS[parameter.mode](parameter.uri),
group=group,
Expand All @@ -381,6 +339,7 @@ async def update(self):
if not self._parameter_updates:
if self.stale_parameters.get():
await self.stale_parameters.set(False)
return

async with self._parameter_update_lock:
parameters = self._parameter_updates.copy()
Expand All @@ -403,17 +362,68 @@ async def update(self):


class EigerDetectorController(EigerSubsystemController):
def __init__(self, connection: HTTPConnection, lock: asyncio.Lock):
super().__init__("detector", connection, lock)
_subsystem = "detector"

# Detector parameters to use in internal logic
trigger_mode = AttrRW(String()) # TODO: Include URI and validate type from API
trigger_exposure = AttrRW(Float(), handler=LogicHandler())

@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)
case ("ints" | "inte", _):
await self.connection.put(command_uri("trigger"))
case _:
raise RuntimeError("Can only do soft trigger in 'ints' or 'inte' mode")

@detector_command
async def initialize(self):
await self.connection.put(command_uri("initialize"))

@detector_command
async def arm(self):
await self.connection.put(command_uri("arm"))

@detector_command
async def disarm(self):
await self.connection.put(command_uri("disarm"))

@detector_command
async def abort(self):
await self.connection.put(command_uri("abort"))

@detector_command
async def cancel(self):
await self.connection.put(command_uri("cancel"))

@classmethod
def _group_and_name(cls, parameter: EigerParameter) -> tuple[str, str]:
if "threshold" in parameter.key:
parts = parameter.key.split("/")
if len(parts) == 3 and parts[1].isnumeric():
group = f"Threshold{parts[1]}"
else:
group = "Threshold"
name = cls._attribute_name(parameter)
return (group, name)
return super()._group_and_name(parameter)
def _group(cls, parameter: EigerParameter) -> str:
if "/" in parameter.key:
group_parts = parameter.key.split("/")[:-1]
# e.g. "threshold/difference/mode" -> ThresholdDifference
return "".join(list(map(str.capitalize, group_parts)))
return super()._group(parameter)


class EigerMonitorController(EigerSubsystemController):
_subsystem = "monitor"

@scan(1)
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"
)
if response.status != 200:
return
else:
image = Image.open(BytesIO(image_bytes))

# TODO: Populate waveform PV to display as image, once supported in PVI
print(np.array(image))


class EigerStreamController(EigerSubsystemController):
_subsystem = "stream"
12 changes: 8 additions & 4 deletions tests/system/test_introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
from fastcs_eiger.eiger_controller import (
EigerController,
EigerDetectorController,
EigerMonitorController,
EigerParameter,
EigerSubsystemController,
EigerStreamController,
)

HERE = Path(__file__).parent
Expand Down Expand Up @@ -96,11 +97,11 @@ async def test_introspection(sim_eiger_controller: EigerController):
subsystem_parameters["DETECTOR"]
)
assert len(detector_attributes) == 76
monitor_attributes = EigerSubsystemController._create_attributes(
monitor_attributes = EigerMonitorController._create_attributes(
subsystem_parameters["MONITOR"]
)
assert len(monitor_attributes) == 7
stream_attributes = EigerSubsystemController._create_attributes(
stream_attributes = EigerStreamController._create_attributes(
subsystem_parameters["STREAM"]
)
assert len(stream_attributes) == 8
Expand All @@ -109,6 +110,9 @@ async def test_introspection(sim_eiger_controller: EigerController):
assert isinstance(detector_attributes["humidity"].datatype, Float)
assert detector_attributes["humidity"]._group == "DetectorStatus"
assert detector_attributes["threshold_2_energy"]._group == "Threshold2"
assert detector_attributes["threshold_energy"]._group == "Threshold"
assert (
detector_attributes["threshold_difference_lower_threshold"]._group
== "ThresholdDifference"
)

await controller.connection.close()
15 changes: 12 additions & 3 deletions tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
MISSING_KEYS,
EigerController,
EigerDetectorController,
EigerSubsystemController,
EigerMonitorController,
EigerStreamController,
)

_lock = asyncio.Lock()
Expand Down Expand Up @@ -43,8 +44,14 @@ async def test_detector_controller(


@pytest.mark.asyncio
async def test_subsystem_controller_initialises(mock_connection):
subsystem_controller = EigerSubsystemController("stream", mock_connection, _lock)
async def test_monitor_controller_initialises(mock_connection):
subsystem_controller = EigerMonitorController(mock_connection, _lock)
await subsystem_controller.initialise()


@pytest.mark.asyncio
async def test_stream_controller_initialises(mock_connection):
subsystem_controller = EigerStreamController(mock_connection, _lock)
await subsystem_controller.initialise()


Expand All @@ -56,6 +63,8 @@ async def test_detector_subsystem_controller(mock_connection):
for attr_name in dir(subsystem_controller):
attr = getattr(subsystem_controller, attr_name)
if isinstance(attr, Attribute) and "threshold" in attr_name:
if attr_name == "threshold_energy":
continue
assert "Threshold" in attr.group


Expand Down

0 comments on commit f11fb04

Please sign in to comment.