mirror of
https://github.com/bec-project/ophyd_devices.git
synced 2025-06-24 19:51:09 +02:00
feat: add psi_device_base class; remove old/redundant base classes
This commit is contained in:
@ -1,523 +0,0 @@
|
||||
"""
|
||||
This module contains the base class for custom device integrations at PSI.
|
||||
Please check the device section in BEC's developer documentation
|
||||
(https://bec.readthedocs.io/en/latest/) for more information about device integration.
|
||||
"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
from bec_lib.file_utils import FileWriter
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Device, DeviceStatus, Kind
|
||||
from ophyd.device import Staged
|
||||
|
||||
from ophyd_devices.utils import bec_utils
|
||||
from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin
|
||||
from ophyd_devices.utils.errors import DeviceStopError, DeviceTimeoutError
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
T = TypeVar("T", bound="BECDeviceBase")
|
||||
|
||||
|
||||
class BECDeviceBaseError(Exception):
|
||||
"""Error class for BECDeviceBase."""
|
||||
|
||||
|
||||
class CustomPrepare(Generic[T]):
|
||||
"""Custom prepare class for beamline specific logic.
|
||||
|
||||
This class provides a set of hooks for beamline specific logic to be implemented.
|
||||
BECDeviceBase will be injected as the parent device.
|
||||
|
||||
To implement custom logic, inherit from this class and implement the desired methods.
|
||||
If the __init__ method is overwritten, please ensure the proper initialisation.
|
||||
It is important to pass the parent device to the custom prepare class as this
|
||||
will allow the custom prepare class to access the parent device with all its signals.
|
||||
"""
|
||||
|
||||
def __init__(self, *_args, parent: T = None, **_kwargs) -> None:
|
||||
"""
|
||||
Initialize the custom prepare class.
|
||||
|
||||
Args:
|
||||
parent (BECDeviceBase): The parent device which gives access to all methods and signals.
|
||||
"""
|
||||
self.parent = parent
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""
|
||||
Hook for beamline specific logic during class initialization.
|
||||
|
||||
This method is called during the initialization of the device class.
|
||||
It should not be used to set any of the class's signals as they may not
|
||||
be connected yet.
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_wait_for_connection(self) -> None:
|
||||
"""
|
||||
Hook for beamline specific logic during the wait_for_connection method.
|
||||
|
||||
This method is called after Ophyd's wait_for_connection method was called,
|
||||
meaning that signals will be connected at this point. It can be used to check
|
||||
signal values, or set default values for those.
|
||||
"""
|
||||
|
||||
def on_stage(self) -> None:
|
||||
"""
|
||||
Hook for beamline specific logic during the stage method.
|
||||
|
||||
This method is called during the stage method of the device class.
|
||||
It is used to implement logic in preparation for a scan."""
|
||||
|
||||
def on_unstage(self) -> None:
|
||||
"""
|
||||
Hook for beamline specific logic during the unstage method.
|
||||
|
||||
This method is called during the unstage method of the device class.
|
||||
It is used to implement logic to clean up the device after a scan.
|
||||
"""
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Hook for beamline specific logic during the stop method."""
|
||||
|
||||
def on_trigger(self) -> None | DeviceStatus:
|
||||
"""
|
||||
Hook for beamline specific logic during the trigger method.
|
||||
|
||||
This method has to be non-blocking, and if time-consuming actions are necessary,
|
||||
they should be implemented asynchronously. Please check the wait_with_status
|
||||
method to implement asynchronous checks.
|
||||
|
||||
The softwareTrigger config value needs to be set to True to indicate to BEC
|
||||
that the device should be triggered from the software during the scan.
|
||||
|
||||
Returns:
|
||||
DeviceStatus: DeviceStatus object that BEC will use to check if the trigger was successful
|
||||
"""
|
||||
|
||||
def on_pre_scan(self) -> None:
|
||||
"""
|
||||
Hook for beamline specific logic during the pre_scan method.
|
||||
|
||||
This method is called from BEC just before the scan core starts, and be used
|
||||
to execute time-critical actions, e.g. arming a detector in case there is a risk of timing out.
|
||||
Note, this method should not be used to implement blocking logic.
|
||||
"""
|
||||
|
||||
def on_complete(self) -> None | DeviceStatus:
|
||||
"""
|
||||
Hook for beamline specific logic during the complete method.
|
||||
|
||||
This method is used to check whether the device has successfully completed its acquisition.
|
||||
For example, a detector may want to check if it has received the correct number of frames, or
|
||||
if it's data backend has finished writing the data to disk. The method should be implemented
|
||||
asynchronously. Please check the wait_with_status method on how to implement asynchronous checks.
|
||||
|
||||
Returns:
|
||||
DeviceStatus: DeviceStatus object that BEC will use to check if the device has successfully completed
|
||||
"""
|
||||
|
||||
def on_kickoff(self) -> None | DeviceStatus:
|
||||
"""
|
||||
Hook for beamline specific logic during the kickoff method.
|
||||
|
||||
This method is called to kickoff the flyer acquisition. BEC will not call this method in general
|
||||
for its scans, but only if the scan explicitly implements this. The method should be non-blocking,
|
||||
and if time-consuming actions are necessary, they should be implemented asynchronously.
|
||||
Please check the wait_with_status method on how to implement asynchronous checks.
|
||||
|
||||
Returns:
|
||||
DeviceStatus: DeviceStatus object that BEC will use to check if the kickoff was successful
|
||||
"""
|
||||
|
||||
def wait_for_signals(
|
||||
self,
|
||||
signal_conditions: list[tuple],
|
||||
timeout: float,
|
||||
check_stopped: bool = False,
|
||||
interval: float = 0.05,
|
||||
all_signals: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Utility method to implement waiting for signals to reach a certain condition. It accepts
|
||||
a list of conditions passed as tuples of executable calls for conditions (get_current_state, condition) to check.
|
||||
It can further be specified if all signals should be True or if any signal should be True.
|
||||
If the timeout is reached, it will return False.
|
||||
|
||||
Args:
|
||||
signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check
|
||||
timeout (float): timeout in seconds
|
||||
check_stopped (bool): True if stopped flag should be checked
|
||||
interval (float): interval in seconds
|
||||
all_signals (bool): True if all signals should be True, False if any signal should be True
|
||||
|
||||
Returns:
|
||||
bool: True if all signals are in the desired state, False if timeout is reached
|
||||
|
||||
Example:
|
||||
>>> self.wait_for_signals(signal_conditions=[(self.acquiring.get, False)], timeout=5, interval=0.05, check_stopped=True, all_signals=True)
|
||||
"""
|
||||
|
||||
timer = 0
|
||||
while True:
|
||||
checks = [
|
||||
get_current_state() == condition
|
||||
for get_current_state, condition in signal_conditions
|
||||
]
|
||||
if check_stopped is True and self.parent.stopped is True:
|
||||
return False
|
||||
if (all_signals and all(checks)) or (not all_signals and any(checks)):
|
||||
return True
|
||||
if timer > timeout:
|
||||
return False
|
||||
time.sleep(interval)
|
||||
timer += interval
|
||||
|
||||
def wait_with_status(
|
||||
self,
|
||||
signal_conditions: list[tuple],
|
||||
timeout: float,
|
||||
check_stopped: bool = False,
|
||||
interval: float = 0.05,
|
||||
all_signals: bool = False,
|
||||
exception_on_timeout: Exception = None,
|
||||
) -> DeviceStatus:
|
||||
"""
|
||||
Utility method to implement asynchronous waiting for signals to reach a certain condition.
|
||||
It accepts a list of conditions passed as tuples of executable calls.
|
||||
|
||||
Please check the wait_for_signals method as it is used to implement the waiting logic.
|
||||
It returns a DeviceStatus object that can be used to check if the asynchronous action is done
|
||||
through 'status.done', and if it was successful through 'status.success'. An exception can be
|
||||
passed to the method which will be raised if the timeout is reached. If the device was stopped
|
||||
during the waiting, a DeviceStopError will be raised.
|
||||
|
||||
Args:
|
||||
signal_conditions (list[tuple]): tuple of executable calls for conditions (get_current_state, condition) to check
|
||||
timeout (float): timeout in seconds
|
||||
check_stopped (bool): True if stopped flag should be checked
|
||||
interval (float): interval in seconds
|
||||
all_signals (bool): True if all signals should be True, False if any signal should be True
|
||||
exception_on_timeout (Exception): Exception to raise on timeout
|
||||
|
||||
Returns:
|
||||
DeviceStatus: DeviceStatus object to check the state of the asynchronous action (status.done, status.success)
|
||||
|
||||
Example:
|
||||
>>> status = self.wait_with_status(signal_conditions=[(self.acquiring.get, False)], timeout=5, interval=0.05, check_stopped=True, all_signals=True)
|
||||
"""
|
||||
if exception_on_timeout is None:
|
||||
exception_on_timeout = DeviceTimeoutError(
|
||||
f"Timeout error for {self.parent.name} while waiting for signals {signal_conditions}"
|
||||
)
|
||||
|
||||
status = DeviceStatus(self.parent)
|
||||
|
||||
# utility function to wrap the wait_for_signals function
|
||||
def wait_for_signals_wrapper(
|
||||
status: DeviceStatus,
|
||||
signal_conditions: list[tuple],
|
||||
timeout: float,
|
||||
check_stopped: bool,
|
||||
interval: float,
|
||||
all_signals: bool,
|
||||
exception_on_timeout: Exception,
|
||||
):
|
||||
try:
|
||||
result = self.wait_for_signals(
|
||||
signal_conditions, timeout, check_stopped, interval, all_signals
|
||||
)
|
||||
if result:
|
||||
status.set_finished()
|
||||
else:
|
||||
if self.parent.stopped:
|
||||
# INFO This will execute a callback to the parent device.stop() method
|
||||
status.set_exception(exc=DeviceStopError(f"{self.parent.name} was stopped"))
|
||||
else:
|
||||
# INFO This will execute a callback to the parent device.stop() method
|
||||
status.set_exception(exc=exception_on_timeout)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
content = traceback.format_exc()
|
||||
logger.warning(
|
||||
f"Error in wait_for_signals in {self.parent.name}; Traceback: {content}"
|
||||
)
|
||||
# INFO This will execute a callback to the parent device.stop() method
|
||||
status.set_exception(exc=exc)
|
||||
|
||||
thread = threading.Thread(
|
||||
target=wait_for_signals_wrapper,
|
||||
args=(
|
||||
status,
|
||||
signal_conditions,
|
||||
timeout,
|
||||
check_stopped,
|
||||
interval,
|
||||
all_signals,
|
||||
exception_on_timeout,
|
||||
),
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
return status
|
||||
|
||||
|
||||
class BECDeviceBase(Device):
|
||||
"""
|
||||
Base class for custom device integrations at PSI. This class wraps around the ophyd's standard
|
||||
set of methods, providing hooks for custom logic to be implemented in the custom_prepare_cls.
|
||||
|
||||
Please check the device section in BEC's developer documentation
|
||||
(https://bec.readthedocs.io/en/latest/) for more information about device integration.
|
||||
"""
|
||||
|
||||
custom_prepare_cls = CustomPrepare
|
||||
|
||||
# All possible subscription types that the Device Manager subscribes to
|
||||
# Run the command _run_subs(sub_type=self.SUB_VALUE, value=value) to trigger
|
||||
# the subscription of type value. Please be aware that the signature of
|
||||
# the subscription callbacks needs to be matched.
|
||||
SUB_READBACK = "readback"
|
||||
SUB_VALUE = "value"
|
||||
SUB_DONE_MOVING = "done_moving"
|
||||
SUB_MOTOR_IS_MOVING = "motor_is_moving"
|
||||
SUB_PROGRESS = "progress"
|
||||
SUB_FILE_EVENT = "file_event"
|
||||
SUB_DEVICE_MONITOR_1D = "device_monitor_1d"
|
||||
SUB_DEVICE_MONITOR_2D = "device_monitor_2d"
|
||||
_default_sub = SUB_VALUE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
prefix: str = "",
|
||||
kind: Kind | None = None,
|
||||
parent=None,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Initialize the device.
|
||||
|
||||
Args:
|
||||
name (str): name of the device
|
||||
prefix (str): prefix of the device
|
||||
kind (Kind): kind of the device
|
||||
parent (Device): parent device
|
||||
device_manager (DeviceManager): device manager. BEC will inject the device manager as a dependency.
|
||||
"""
|
||||
super().__init__(prefix=prefix, name=name, kind=kind, parent=parent, **kwargs)
|
||||
self._stopped = False
|
||||
self.service_cfg = None
|
||||
self.scaninfo = None
|
||||
self.filewriter = None
|
||||
|
||||
if not issubclass(self.custom_prepare_cls, CustomPrepare):
|
||||
raise BECDeviceBaseError(
|
||||
f"Custom prepare class must be subclass of CustomDetectorMixin, provided: {self.custom_prepare_cls}"
|
||||
)
|
||||
self.custom_prepare = self.custom_prepare_cls(parent=self, **kwargs)
|
||||
|
||||
if device_manager:
|
||||
self._update_service_config()
|
||||
self.device_manager = device_manager
|
||||
else:
|
||||
# If device manager is not provided through dependency injection
|
||||
# A mock device manager is created. This is necessary to be able
|
||||
# to use the device class in a standalone context without BEC.
|
||||
self.device_manager = bec_utils.DMMock()
|
||||
base_path = kwargs["basepath"] if "basepath" in kwargs else "."
|
||||
self.service_cfg = {"base_path": os.path.abspath(base_path)}
|
||||
|
||||
self.connector = self.device_manager.connector
|
||||
self._update_scaninfo()
|
||||
self._update_filewriter()
|
||||
self._init()
|
||||
|
||||
@property
|
||||
def stopped(self) -> bool:
|
||||
"""Property to indicate if the device was stopped."""
|
||||
return self._stopped
|
||||
|
||||
@stopped.setter
|
||||
def stopped(self, value: bool) -> None:
|
||||
self._stopped = value
|
||||
|
||||
def _update_filewriter(self) -> None:
|
||||
"""Initialise the file writer utility class."""
|
||||
self.filewriter = FileWriter(service_config=self.service_cfg, connector=self.connector)
|
||||
|
||||
def _update_scaninfo(self) -> None:
|
||||
"""Initialise the utility class to get scan metadata from BEC."""
|
||||
self.scaninfo = BecScaninfoMixin(self.device_manager)
|
||||
self.scaninfo.load_scan_metadata()
|
||||
|
||||
def _update_service_config(self) -> None:
|
||||
"""Update the Service Config. This has the info on where REDIS and other services are running."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from bec_lib.bec_service import SERVICE_CONFIG
|
||||
|
||||
if SERVICE_CONFIG:
|
||||
self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"]
|
||||
return
|
||||
self.service_cfg = {"base_path": os.path.abspath(".")}
|
||||
|
||||
def check_scan_id(self) -> None:
|
||||
"""
|
||||
Check if the scan ID has changed, if yes, set the stopped property to True.
|
||||
|
||||
The idea is that if the scan ID on the device and the scan ID from BEC are different,
|
||||
the device is out of sync with the scan and should be stopped.
|
||||
"""
|
||||
old_scan_id = self.scaninfo.scan_id
|
||||
self.scaninfo.load_scan_metadata()
|
||||
if self.scaninfo.scan_id != old_scan_id:
|
||||
self.stopped = True
|
||||
|
||||
def _init(self) -> None:
|
||||
"""
|
||||
Hook for beamline specific logic during class initialization.
|
||||
|
||||
Please to not set any of the class's signals during intialisation, but
|
||||
instead use the wait_for_connection.
|
||||
"""
|
||||
self.custom_prepare.on_init()
|
||||
|
||||
def wait_for_connection(self, all_signals=False, timeout=5) -> None:
|
||||
"""
|
||||
Thin wrapper around ophyd's wait_for_connection.
|
||||
|
||||
Calling Ophyd's wait_for_connection will ensure that signals are connected. BEC
|
||||
will call this method if a device is not connected yet. This method should
|
||||
be used to set up default values for signals, or to check if the device signals
|
||||
are in the expected state.
|
||||
|
||||
Args:
|
||||
all_signals (bool): True if all signals should be considered. Default is False.
|
||||
timeout (float): timeout in seconds. Default is 5 seconds.
|
||||
"""
|
||||
super().wait_for_connection(all_signals, timeout)
|
||||
self.custom_prepare.on_wait_for_connection()
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
"""
|
||||
Thin wrapper around ophyd's stage, the method called in preparation for a scan.
|
||||
|
||||
Stage is idempotent, if staged twice it should raise (we let ophyd.Device handle the raise here).
|
||||
Other that that, we reset the stopped property in case the device was stopped before, and
|
||||
pull the latest scan metadata from BEC. Ater that, we allow for beamline specific logic to
|
||||
be implemented through the custom_prepare.on_stage method.
|
||||
|
||||
Returns:
|
||||
list(object): list of objects that were staged
|
||||
"""
|
||||
if self._staged != Staged.no:
|
||||
return super().stage()
|
||||
self.stopped = False
|
||||
self.scaninfo.load_scan_metadata()
|
||||
self.custom_prepare.on_stage()
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
"""
|
||||
This wrapper around ophyd's unstage method, which is called to clean up the device.
|
||||
|
||||
It must be possible to call unstage multiple times without raising an exception. It should
|
||||
be implemented to clean up the device if it is in a staged state.
|
||||
|
||||
Beamline specific logic can be implemented through the custom_prepare.on_unstage method.
|
||||
|
||||
Returns:
|
||||
list(object): list of objects that were unstaged
|
||||
"""
|
||||
self.check_scan_id()
|
||||
self.custom_prepare.on_unstage()
|
||||
self.stopped = False
|
||||
return super().unstage()
|
||||
|
||||
def pre_scan(self) -> None:
|
||||
"""
|
||||
Pre-scan is a method introduced by BEC that is not native to the ophyd interface.
|
||||
|
||||
This method is called from BEC just before the scan core starts, and therefore should only
|
||||
implement time-critical actions. I.e. Arming a detector in case there is a risk of timing out.
|
||||
"""
|
||||
self.custom_prepare.on_pre_scan()
|
||||
|
||||
def trigger(self) -> DeviceStatus:
|
||||
"""
|
||||
Thin wrapper around the trigger method of the device, for which the config value
|
||||
softwareTrigger needs to be set to True, which will indicate to BEC that the device
|
||||
should be triggered from the software during the scan.
|
||||
|
||||
Custom logic should be implemented non-blocking, i.e. be fast, or implemented asynchroniously.
|
||||
|
||||
Returns:
|
||||
DeviceStatus: DeviceStatus object that BEC will use to check if the trigger was successful.
|
||||
"""
|
||||
# pylint: disable=assignment-from-no-return
|
||||
status = self.custom_prepare.on_trigger()
|
||||
if isinstance(status, DeviceStatus):
|
||||
return status
|
||||
return super().trigger()
|
||||
|
||||
def complete(self) -> DeviceStatus:
|
||||
"""
|
||||
Thin wrapper around ophyd's complete method. Complete is called once the scan core
|
||||
has finished, but before the scan is closed. It will be called before unstage.
|
||||
It can also be used for fly scans to track the status of the flyer, and indicate if the
|
||||
flyer has completed.
|
||||
|
||||
The method is used to check whether the device has successfully completed the acquisition.
|
||||
Actions are implemented in custom_prepare.on_complete since they are beamline specific.
|
||||
|
||||
This method has to be non-blocking. If checks are necessary, they should be implemented asynchronously.
|
||||
|
||||
Returns:
|
||||
DeviceStatus: DeviceStatus object that BEC will use to check if the device has successfully completed.
|
||||
"""
|
||||
# pylint: disable=assignment-from-no-return
|
||||
status = self.custom_prepare.on_complete()
|
||||
if isinstance(status, DeviceStatus):
|
||||
return status
|
||||
status = DeviceStatus(self)
|
||||
status.set_finished()
|
||||
return status
|
||||
|
||||
def stop(self, *, success=False) -> None:
|
||||
"""Stop the device.
|
||||
|
||||
Args:
|
||||
success (bool): Argument from ophyd's stop method. Default is False.
|
||||
"""
|
||||
self.custom_prepare.on_stop()
|
||||
super().stop(success=success)
|
||||
self.stopped = True
|
||||
|
||||
def kickoff(self) -> DeviceStatus:
|
||||
"""
|
||||
This wrapper around Ophyd's kickoff method.
|
||||
|
||||
Kickoff is a method native to the flyer interface of Ophyd. It is called to
|
||||
start the flyer acquisition. This method is not called by BEC in general, but
|
||||
only if the scan explicitly implements this.
|
||||
|
||||
The method should be non-blocking, and if time-consuming actions are necessary,
|
||||
they should be implemented asynchronously.
|
||||
|
||||
Returns:
|
||||
DeviceStatus: DeviceStatus object that BEC will use to check if the kickoff was successful.
|
||||
"""
|
||||
# pylint: disable=assignment-from-no-return
|
||||
status = self.custom_prepare.on_kickoff()
|
||||
if isinstance(status, DeviceStatus):
|
||||
return status
|
||||
status = DeviceStatus(self)
|
||||
status.set_finished()
|
||||
return status
|
@ -1,105 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from bec_lib import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import EpicsMotor
|
||||
from typeguard import typechecked
|
||||
|
||||
from ophyd_devices.interfaces.protocols.bec_protocols import BECRotationProtocol
|
||||
from ophyd_devices.utils.bec_utils import ConfigSignal
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class OphtyRotationBaseError(Exception):
|
||||
"""Exception specific for implmenetation of rotation stages."""
|
||||
|
||||
|
||||
class OphydRotationBase(BECRotationProtocol, ABC):
|
||||
|
||||
allow_mod360 = Cpt(ConfigSignal, name="allow_mod360", value=False, kind="config")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Base class to implement functionality specific for rotation devices.
|
||||
|
||||
Childrens should override the instance attributes:
|
||||
- has_mod360
|
||||
- has_freerun
|
||||
- valid_rotation_modes
|
||||
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
self._has_mod360 = False
|
||||
self._has_freerun = False
|
||||
self._valid_rotation_modes = []
|
||||
if "allow_mod360" in kwargs:
|
||||
if not isinstance(kwargs["allow_mod360"], bool):
|
||||
raise ValueError("allow_mod360 must be a boolean")
|
||||
self.allow_mod360.put(kwargs["allow_mod360"])
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@abstractmethod
|
||||
def apply_mod360(self) -> None:
|
||||
"""Method to apply the modulus 360 operation on the specific device.
|
||||
|
||||
Childrens should override this method
|
||||
"""
|
||||
|
||||
@property
|
||||
def has_mod360(self) -> bool:
|
||||
"""Property to check if the device has mod360 operation.
|
||||
|
||||
ReadOnly property, childrens should override this method.
|
||||
"""
|
||||
return self._has_mod360
|
||||
|
||||
@property
|
||||
def has_freerun(self) -> bool:
|
||||
"""Property to check if the device has freerun operation.
|
||||
|
||||
ReadOnly property, childrens should override this method.
|
||||
"""
|
||||
return self._has_freerun
|
||||
|
||||
@property
|
||||
def valid_rotation_modes(self) -> list:
|
||||
"""Method to get the valid rotation modes for the specific device."""
|
||||
return self._valid_rotation_modes
|
||||
|
||||
@typechecked
|
||||
@valid_rotation_modes.setter
|
||||
def valid_rotation_modes(self, value: list[str]):
|
||||
"""Method to set the valid rotation modes for the specific device."""
|
||||
self._valid_rotation_modes = value
|
||||
return self._valid_rotation_modes
|
||||
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
class EpicsRotationBase(OphydRotationBase, EpicsMotor):
|
||||
"""Class for Epics rotation devices."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._has_freerun = True
|
||||
self._has_freerun = True
|
||||
self._valid_rotation_modes = ["target", "radiography"]
|
||||
|
||||
def apply_mod360(self) -> None:
|
||||
"""Apply modulos 360 operation for EpicsMotorRecord.
|
||||
|
||||
EpicsMotor has the function "set_current_position" which can be used for this purpose.
|
||||
In addition, there is a check if mod360 is allowed and available.
|
||||
"""
|
||||
if self.has_mod360 and self.allow_mod360.get():
|
||||
cur_val = self.user_readback.get()
|
||||
new_val = cur_val % 360
|
||||
try:
|
||||
self.set_current_position(new_val)
|
||||
except Exception as exc:
|
||||
error_msg = f"Failed to set new position {new_val} from {cur_val} on device {self.name} with error {exc}"
|
||||
raise OphtyRotationBaseError(error_msg) from exc
|
||||
return
|
||||
logger.info(
|
||||
f"Did not apply mod360 for device {self.name} with has_mod={self.has_mod360} and allow_mod={self.allow_mod360.get()}"
|
||||
)
|
@ -1,65 +0,0 @@
|
||||
from bec_lib import messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Kind
|
||||
|
||||
from ophyd_devices.interfaces.base_classes.bec_device_base import BECDeviceBase, CustomPrepare
|
||||
from ophyd_devices.sim.sim_signals import SetableSignal
|
||||
|
||||
|
||||
class CustomDetectorMixin(CustomPrepare):
|
||||
"""Deprecated, use CustomPrepare instead. Here for backwards compatibility."""
|
||||
|
||||
def publish_file_location(
|
||||
self,
|
||||
done: bool,
|
||||
successful: bool,
|
||||
filepath: str = None,
|
||||
hinted_locations: dict = None,
|
||||
metadata: dict = None,
|
||||
) -> None:
|
||||
"""
|
||||
Publish the filepath to REDIS.
|
||||
|
||||
We publish two events here:
|
||||
- file_event: event for the filewriter
|
||||
- public_file: event for any secondary service (e.g. radial integ code)
|
||||
|
||||
Args:
|
||||
done (bool): True if scan is finished
|
||||
successful (bool): True if scan was successful
|
||||
filepath (str): Optional, filepath to publish. If None, it will be taken from self.parent.filepath.get()
|
||||
hinted_locations (dict): Optional, dictionary with hinted locations; {dev_name : h5_entry}
|
||||
metadata (dict): additional metadata to publish
|
||||
"""
|
||||
if metadata is None:
|
||||
metadata = {}
|
||||
|
||||
if filepath is None:
|
||||
file_path = self.parent.filepath.get()
|
||||
|
||||
msg = messages.FileMessage(
|
||||
file_path=self.parent.filepath.get(),
|
||||
hinted_locations=hinted_locations,
|
||||
done=done,
|
||||
successful=successful,
|
||||
metadata=metadata,
|
||||
)
|
||||
pipe = self.parent.connector.pipeline()
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name),
|
||||
msg,
|
||||
pipe=pipe,
|
||||
)
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
|
||||
)
|
||||
pipe.execute()
|
||||
|
||||
|
||||
class PSIDetectorBase(BECDeviceBase):
|
||||
"""Deprecated, use BECDeviceBase instead. Here for backwards compatibility."""
|
||||
|
||||
custom_prepare_cls = CustomDetectorMixin
|
||||
|
||||
filepath = Cpt(SetableSignal, value="", kind=Kind.config)
|
176
ophyd_devices/interfaces/base_classes/psi_device_base.py
Normal file
176
ophyd_devices/interfaces/base_classes/psi_device_base.py
Normal file
@ -0,0 +1,176 @@
|
||||
"""
|
||||
Consider using the bec_device_base name for the base class.
|
||||
I will use this name instead here to simplify comparisons between the two approaches
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ophyd import Device, DeviceStatus, Staged, StatusBase
|
||||
|
||||
from ophyd_devices.tests.utils import get_mock_scan_info
|
||||
from ophyd_devices.utils.psi_device_base_utils import FileHandler, TaskHandler
|
||||
|
||||
|
||||
class PSIDeviceBase(Device):
|
||||
"""
|
||||
Base class for all PSI ophyd devices to ensure consistent configuration
|
||||
and communication with BEC services.
|
||||
"""
|
||||
|
||||
# These are all possible subscription types that the device_manager supports
|
||||
# and automatically subscribes to
|
||||
SUB_READBACK = "readback"
|
||||
SUB_VALUE = "value"
|
||||
SUB_DONE_MOVING = "done_moving"
|
||||
SUB_MOTOR_IS_MOVING = "motor_is_moving"
|
||||
SUB_PROGRESS = "progress"
|
||||
SUB_FILE_EVENT = "file_event"
|
||||
SUB_DEVICE_MONITOR_1D = "device_monitor_1d"
|
||||
SUB_DEVICE_MONITOR_2D = "device_monitor_2d"
|
||||
_default_sub = SUB_VALUE
|
||||
|
||||
def __init__(self, name: str, scan_info=None, **kwargs):
|
||||
"""
|
||||
Initialize the PSI Device Base class.
|
||||
|
||||
Args:
|
||||
name (str) : Name of the device
|
||||
scan_info (ScanInfo): The scan info to use.
|
||||
"""
|
||||
super().__init__(name=name, **kwargs)
|
||||
self._stopped = False
|
||||
self.task_handler = TaskHandler(parent=self)
|
||||
if scan_info is None:
|
||||
scan_info = get_mock_scan_info()
|
||||
self.scan_info = scan_info
|
||||
self.file_utils = FileHandler()
|
||||
self.on_init()
|
||||
|
||||
########################################
|
||||
# Additional Properties and Attributes #
|
||||
########################################
|
||||
|
||||
@property
|
||||
def destroyed(self) -> bool:
|
||||
"""Check if the device has been destroyed."""
|
||||
return self._destroyed
|
||||
|
||||
@property
|
||||
def staged(self) -> Staged:
|
||||
"""Check if the device has been staged."""
|
||||
return self._staged
|
||||
|
||||
@property
|
||||
def stopped(self) -> bool:
|
||||
"""Check if the device has been stopped."""
|
||||
return self._stopped
|
||||
|
||||
@stopped.setter
|
||||
def stopped(self, value: bool):
|
||||
self._stopped = value
|
||||
|
||||
########################################
|
||||
# Wrapper around Device class methods #
|
||||
########################################
|
||||
|
||||
def stage(self) -> list[object] | StatusBase:
|
||||
"""Stage the device."""
|
||||
if self.staged != Staged.no:
|
||||
return super().stage()
|
||||
self.stopped = False
|
||||
super_staged = super().stage()
|
||||
status = self.on_stage() # pylint: disable=assignment-from-no-return
|
||||
if isinstance(status, StatusBase):
|
||||
return status
|
||||
return super_staged
|
||||
|
||||
def unstage(self) -> list[object] | StatusBase:
|
||||
"""Unstage the device."""
|
||||
super_unstage = super().unstage()
|
||||
status = self.on_unstage() # pylint: disable=assignment-from-no-return
|
||||
if isinstance(status, StatusBase):
|
||||
return status
|
||||
return super_unstage
|
||||
|
||||
def pre_scan(self) -> StatusBase | None:
|
||||
"""Pre-scan function."""
|
||||
status = self.on_pre_scan() # pylint: disable=assignment-from-no-return
|
||||
return status
|
||||
|
||||
def trigger(self) -> DeviceStatus:
|
||||
"""Trigger the device."""
|
||||
super_trigger = super().trigger()
|
||||
status = self.on_trigger() # pylint: disable=assignment-from-no-return
|
||||
return status if status else super_trigger
|
||||
|
||||
def complete(self) -> DeviceStatus:
|
||||
"""Complete the device."""
|
||||
status = self.on_complete() # pylint: disable=assignment-from-no-return
|
||||
if isinstance(status, StatusBase):
|
||||
return status
|
||||
status = DeviceStatus(self)
|
||||
status.set_finished()
|
||||
return status
|
||||
|
||||
def kickoff(self) -> DeviceStatus:
|
||||
"""Kickoff the device."""
|
||||
status = self.on_kickoff() # pylint: disable=assignment-from-no-return
|
||||
if isinstance(status, StatusBase):
|
||||
return status
|
||||
status = DeviceStatus(self)
|
||||
status.set_finished()
|
||||
return status
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def stop(self, success: bool = False) -> None:
|
||||
"""Stop the device.
|
||||
|
||||
Args:
|
||||
success (bool): True if the device was stopped successfully.
|
||||
"""
|
||||
self.on_stop()
|
||||
super().stop(success=success)
|
||||
self.stopped = True
|
||||
|
||||
########################################
|
||||
# Beamline Specific Implementations #
|
||||
########################################
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""
|
||||
Called when the device is initialized.
|
||||
|
||||
No siganls are connected at this point,
|
||||
thus should not be set here but in on_connected instead.
|
||||
"""
|
||||
|
||||
def on_connected(self) -> None:
|
||||
"""
|
||||
Called after the device is connected and its signals are connected.
|
||||
Default values for signals should be set here.
|
||||
"""
|
||||
|
||||
def on_stage(self) -> DeviceStatus | None:
|
||||
"""
|
||||
Called while staging the device.
|
||||
|
||||
Information about the upcoming scan can be accessed from the scan_info object.
|
||||
"""
|
||||
|
||||
def on_unstage(self) -> DeviceStatus | None:
|
||||
"""Called while unstaging the device."""
|
||||
|
||||
def on_pre_scan(self) -> DeviceStatus | None:
|
||||
"""Called right before the scan starts on all devices automatically."""
|
||||
|
||||
def on_trigger(self) -> DeviceStatus | None:
|
||||
"""Called when the device is triggered."""
|
||||
|
||||
def on_complete(self) -> DeviceStatus | None:
|
||||
"""Called to inquire if a device has completed a scans."""
|
||||
|
||||
def on_kickoff(self) -> DeviceStatus | None:
|
||||
"""Called to kickoff a device for a fly scan. Has to be called explicitly."""
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Called when the device is stopped."""
|
@ -1,7 +1,9 @@
|
||||
""" This module provides a range of protocols that describe the expected interface for different types of devices.
|
||||
""" This module provides a range of protocols that describe the expected
|
||||
interface for different types of devices.
|
||||
|
||||
The protocols below can be used as teamplates for functionality to be implemeted by different type of devices.
|
||||
They further facilitate runtime checks on devices and provide a minimum set of properties required for a device to be loadable by BEC.
|
||||
The protocols below can be used as teamplates for functionality to be implemeted
|
||||
by different type of devices. They further facilitate runtime checks on devices
|
||||
and provide a minimum set of properties required for a device to be loadable by BEC.
|
||||
|
||||
The protocols are:
|
||||
- BECBaseProtocol: Protocol for devices in BEC. All devices must at least implement this protocol.
|
||||
@ -11,17 +13,15 @@ The protocols are:
|
||||
- BECPositionerProtocol: Protocol for positioners.
|
||||
- BECFlyerProtocol: Protocol with for flyers.
|
||||
|
||||
Keep in mind, that a device of type flyer should generally also implement the BECDeviceProtocol that provides the required functionality for scans.
|
||||
Flyers in addition, also implement the BECFlyerProtocol. Similarly, positioners should also implement the BECDeviceProtocol and BECPositionerProtocol.
|
||||
Keep in mind, that a device of type flyer should generally also implement the BECDeviceProtocol
|
||||
with the functionality needed for scans. In addition, flyers also implement the BECFlyerProtocol.
|
||||
Similarly, positioners should also implement the BECDeviceProtocol and BECPositionerProtocol.
|
||||
|
||||
"""
|
||||
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
from bec_lib.file_utils import FileWriter
|
||||
from ophyd import Component, DeviceStatus, Kind, Staged
|
||||
|
||||
from ophyd_devices.utils import bec_scaninfo_mixin
|
||||
from ophyd import DeviceStatus, Kind, Staged
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
@ -349,8 +349,8 @@ class BECPositionerProtocol(BECDeviceProtocol, Protocol):
|
||||
|
||||
def move(self, position: float) -> DeviceStatus:
|
||||
"""Move method for positioners.
|
||||
The returned DeviceStatus is marked as done once the positioner has reached the target position.
|
||||
DeviceStatus.wait() can be used to block until the move is completed.
|
||||
The returned DeviceStatus is marked as done once the positioner has reached the target
|
||||
position. DeviceStatus.wait() can be used to block until the move is completed.
|
||||
|
||||
Args:
|
||||
position: position to move to
|
||||
|
@ -1,161 +1,23 @@
|
||||
import traceback
|
||||
from threading import Thread
|
||||
""" Simulated 2D camera device"""
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import DeviceStatus, Kind
|
||||
from ophyd import Device, Kind, StatusBase
|
||||
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin,
|
||||
PSIDetectorBase,
|
||||
)
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
from ophyd_devices.sim.sim_data import SimulatedDataCamera
|
||||
from ophyd_devices.sim.sim_signals import ReadOnlySignal, SetableSignal
|
||||
from ophyd_devices.sim.sim_utils import H5Writer
|
||||
from ophyd_devices.utils.errors import DeviceStopError
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class SimCameraSetup(CustomDetectorMixin):
|
||||
"""Mixin class for the SimCamera device."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._thread_trigger = None
|
||||
self._thread_complete = None
|
||||
self.file_path = None
|
||||
|
||||
def on_trigger(self) -> None:
|
||||
"""Trigger the camera to acquire images.
|
||||
|
||||
This method can be called from BEC during a scan. It will acquire images and send them to BEC.
|
||||
Whether the trigger is send from BEC is determined by the softwareTrigger argument in the device config.
|
||||
|
||||
Here, we also run a callback on SUB_MONITOR to send the image data the device_monitor endpoint in BEC.
|
||||
"""
|
||||
status = DeviceStatus(self.parent)
|
||||
|
||||
def on_trigger_call(status: DeviceStatus) -> None:
|
||||
try:
|
||||
for _ in range(self.parent.burst.get()):
|
||||
data = self.parent.image.get()
|
||||
# pylint: disable=protected-access
|
||||
self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=data)
|
||||
if self.parent.stopped:
|
||||
raise DeviceStopError(f"{self.parent.name} was stopped")
|
||||
if self.parent.write_to_disk.get():
|
||||
self.parent.h5_writer.receive_data(data)
|
||||
status.set_finished()
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
content = traceback.format_exc()
|
||||
logger.warning(
|
||||
f"Error in on_trigger_call in device {self.parent.name}. Error traceback: {content}"
|
||||
)
|
||||
status.set_exception(exc)
|
||||
|
||||
self._thread_trigger = Thread(target=on_trigger_call, args=(status,))
|
||||
self._thread_trigger.start()
|
||||
return status
|
||||
|
||||
def on_stage(self) -> None:
|
||||
"""Stage the camera for upcoming scan
|
||||
|
||||
This method is called from BEC in preparation of a scan.
|
||||
It receives metadata about the scan from BEC,
|
||||
compiles it and prepares the camera for the scan.
|
||||
|
||||
FYI: No data is written to disk in the simulation, but upon each trigger it
|
||||
is published to the device_monitor endpoint in REDIS.
|
||||
"""
|
||||
self.file_path = self.parent.filewriter.compile_full_filename(f"{self.parent.name}")
|
||||
|
||||
self.parent.frames.set(
|
||||
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
|
||||
)
|
||||
self.parent.exp_time.set(self.parent.scaninfo.exp_time)
|
||||
self.parent.burst.set(self.parent.scaninfo.frames_per_trigger)
|
||||
if self.parent.write_to_disk.get():
|
||||
self.parent.h5_writer.on_stage(file_path=self.file_path, h5_entry="/entry/data/data")
|
||||
self.parent._run_subs(
|
||||
sub_type=self.parent.SUB_FILE_EVENT,
|
||||
file_path=self.file_path,
|
||||
done=False,
|
||||
successful=False,
|
||||
hinted_location={"data": "/entry/data/data"},
|
||||
)
|
||||
self.parent.stopped = False
|
||||
|
||||
def on_complete(self) -> None:
|
||||
"""Complete the motion of the simulated device."""
|
||||
status = DeviceStatus(self.parent)
|
||||
|
||||
def on_complete_call(status: DeviceStatus) -> None:
|
||||
try:
|
||||
if self.parent.write_to_disk.get():
|
||||
self.parent.h5_writer.on_complete()
|
||||
self.parent._run_subs(
|
||||
sub_type=self.parent.SUB_FILE_EVENT,
|
||||
file_path=self.file_path,
|
||||
done=True,
|
||||
successful=True,
|
||||
hinted_location={"data": "/entry/data/data"},
|
||||
)
|
||||
if self.parent.stopped:
|
||||
raise DeviceStopError(f"{self.parent.name} was stopped")
|
||||
status.set_finished()
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
content = traceback.format_exc()
|
||||
logger.warning(
|
||||
f"Error in on_complete call in device {self.parent.name}. Error traceback: {content}"
|
||||
)
|
||||
status.set_exception(exc)
|
||||
|
||||
self._thread_complete = Thread(target=on_complete_call, args=(status,), daemon=True)
|
||||
self._thread_complete.start()
|
||||
return status
|
||||
|
||||
def on_unstage(self):
|
||||
"""Unstage the camera device."""
|
||||
if self.parent.write_to_disk.get():
|
||||
self.parent.h5_writer.on_unstage()
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Stop the camera acquisition."""
|
||||
if self._thread_trigger:
|
||||
self._thread_trigger.join()
|
||||
if self._thread_complete:
|
||||
self._thread_complete.join()
|
||||
self.on_unstage()
|
||||
self._thread_trigger = None
|
||||
self._thread_complete = None
|
||||
|
||||
|
||||
class SimCamera(PSIDetectorBase):
|
||||
"""A simulated device mimic any 2D camera.
|
||||
|
||||
It's image is a computed signal, which is configurable by the user and from the command line.
|
||||
The corresponding simulation class is sim_cls=SimulatedDataCamera, more details on defaults within the simulation class.
|
||||
|
||||
>>> camera = SimCamera(name="camera")
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name (string) : Name of the device. This is the only required argmuent, passed on to all signals of the device.
|
||||
precision (integer) : Precision of the readback in digits, written to .describe(). Default is 3 digits.
|
||||
sim_init (dict) : Dictionary to initiate parameters of the simulation, check simulation type defaults for more details.
|
||||
parent : Parent device, optional, is used internally if this signal/device is part of a larger device.
|
||||
kind : A member the Kind IntEnum (or equivalent integer), optional. Default is Kind.normal. See Kind for options.
|
||||
device_manager : DeviceManager from BEC, optional . Within startup of simulation, device_manager is passed on automatically.
|
||||
|
||||
"""
|
||||
class SimCameraControl(Device):
|
||||
"""SimCamera Control layer"""
|
||||
|
||||
USER_ACCESS = ["sim", "registered_proxies"]
|
||||
|
||||
custom_prepare_cls = SimCameraSetup
|
||||
sim_cls = SimulatedDataCamera
|
||||
SHAPE = (100, 100)
|
||||
BIT_DEPTH = np.uint16
|
||||
@ -178,16 +40,13 @@ class SimCamera(PSIDetectorBase):
|
||||
)
|
||||
write_to_disk = Cpt(SetableSignal, name="write_to_disk", value=False, kind=Kind.config)
|
||||
|
||||
def __init__(
|
||||
self, name, *, kind=None, parent=None, sim_init: dict = None, device_manager=None, **kwargs
|
||||
):
|
||||
def __init__(self, name, *, parent=None, sim_init: dict = None, device_manager=None, **kwargs):
|
||||
self.sim_init = sim_init
|
||||
self.device_manager = device_manager
|
||||
self._registered_proxies = {}
|
||||
self.sim = self.sim_cls(parent=self, **kwargs)
|
||||
self.h5_writer = H5Writer()
|
||||
super().__init__(
|
||||
name=name, parent=parent, kind=kind, device_manager=device_manager, **kwargs
|
||||
)
|
||||
super().__init__(name=name, parent=parent, **kwargs)
|
||||
if self.sim_init:
|
||||
self.sim.set_init(self.sim_init)
|
||||
|
||||
@ -195,3 +54,104 @@ class SimCamera(PSIDetectorBase):
|
||||
def registered_proxies(self) -> None:
|
||||
"""Dictionary of registered signal_names and proxies."""
|
||||
return self._registered_proxies
|
||||
|
||||
|
||||
class SimCamera(PSIDeviceBase, SimCameraControl):
|
||||
"""A simulated device mimic any 2D camera.
|
||||
|
||||
It's image is a computed signal, which is configurable by the user and from the command line.
|
||||
The corresponding simulation class is sim_cls=SimulatedDataCamera, more details on defaults within the simulation class.
|
||||
|
||||
>>> camera = SimCamera(name="camera")
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name (string) : Name of the device. This is the only required argmuent, passed on to all signals of the device.
|
||||
precision (integer) : Precision of the readback in digits, written to .describe(). Default is 3 digits.
|
||||
sim_init (dict) : Dictionary to initiate parameters of the simulation, check simulation type defaults for more details.
|
||||
parent : Parent device, optional, is used internally if this signal/device is part of a larger device.
|
||||
kind : A member the Kind IntEnum (or equivalent integer), optional. Default is Kind.normal. See Kind for options.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, scan_info=None, device_manager=None, **kwargs):
|
||||
super().__init__(name=name, scan_info=scan_info, device_manager=device_manager, **kwargs)
|
||||
self.file_path = None
|
||||
|
||||
def on_trigger(self) -> StatusBase:
|
||||
"""Trigger the camera to acquire images.
|
||||
|
||||
This method can be called from BEC during a scan. It will acquire images and send them to BEC.
|
||||
Whether the trigger is send from BEC is determined by the softwareTrigger argument in the device config.
|
||||
|
||||
Here, we also run a callback on SUB_MONITOR to send the image data the device_monitor endpoint in BEC.
|
||||
"""
|
||||
|
||||
def trigger_cam() -> None:
|
||||
"""Trigger the camera to acquire images."""
|
||||
for _ in range(self.burst.get()):
|
||||
data = self.image.get()
|
||||
# pylint: disable=protected-access
|
||||
self._run_subs(sub_type=self.SUB_MONITOR, value=data)
|
||||
if self.write_to_disk.get():
|
||||
self.h5_writer.receive_data(data)
|
||||
|
||||
status = self.task_handler.submit_task(trigger_cam)
|
||||
return status
|
||||
|
||||
def on_stage(self) -> None:
|
||||
"""Stage the camera for upcoming scan
|
||||
|
||||
This method is called from BEC in preparation of a scan.
|
||||
It receives metadata about the scan from BEC,
|
||||
compiles it and prepares the camera for the scan.
|
||||
|
||||
FYI: No data is written to disk in the simulation, but upon each trigger it
|
||||
is published to the device_monitor endpoint in REDIS.
|
||||
"""
|
||||
self.file_path = self.file_utils.get_file_path(
|
||||
scan_status_msg=self.scan_info.msg, name=self.name
|
||||
)
|
||||
self.frames.set(
|
||||
self.scan_info.msg.num_points * self.scan_info.msg.scan_parameters["frames_per_trigger"]
|
||||
)
|
||||
self.exp_time.set(self.scan_info.msg.scan_parameters["exp_time"])
|
||||
self.burst.set(self.scan_info.msg.scan_parameters["frames_per_trigger"])
|
||||
if self.write_to_disk.get():
|
||||
self.h5_writer.on_stage(file_path=self.file_path, h5_entry="/entry/data/data")
|
||||
# pylint: disable=protected-access
|
||||
self._run_subs(
|
||||
sub_type=self.SUB_FILE_EVENT,
|
||||
file_path=self.file_path,
|
||||
done=False,
|
||||
successful=False,
|
||||
hinted_location={"data": "/entry/data/data"},
|
||||
)
|
||||
|
||||
def on_complete(self) -> StatusBase:
|
||||
"""Complete the motion of the simulated device."""
|
||||
|
||||
def complete_cam():
|
||||
"""Complete the camera acquisition."""
|
||||
if self.write_to_disk.get():
|
||||
self.h5_writer.on_complete()
|
||||
self._run_subs(
|
||||
sub_type=self.SUB_FILE_EVENT,
|
||||
file_path=self.file_path,
|
||||
done=True,
|
||||
successful=True,
|
||||
hinted_location={"data": "/entry/data/data"},
|
||||
)
|
||||
|
||||
status = self.task_handler.submit_task(complete_cam)
|
||||
return status
|
||||
|
||||
def on_unstage(self) -> None:
|
||||
"""Unstage the camera device."""
|
||||
if self.write_to_disk.get():
|
||||
self.h5_writer.on_unstage()
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Stop the camera acquisition."""
|
||||
self.task_handler.shutdown()
|
||||
self.on_unstage()
|
||||
|
@ -106,7 +106,7 @@ class SimulatedDataBase(ABC):
|
||||
def execute_simulation_method(self, *args, method=None, signal_name: str = "", **kwargs) -> any:
|
||||
"""
|
||||
Execute either the provided method or reroutes the method execution
|
||||
to a device proxy in case it is registered in self.parentregistered_proxies.
|
||||
to a device proxy in case it is registered in self.parent.registered_proxies.
|
||||
"""
|
||||
if self.registered_proxies and self.parent.device_manager:
|
||||
for proxy_name, signal in self.registered_proxies.items():
|
||||
|
@ -1,22 +1,16 @@
|
||||
"""Module for simulated monitor devices."""
|
||||
|
||||
import traceback
|
||||
from threading import Thread
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, DeviceStatus, Kind
|
||||
from ophyd import Device, Kind, StatusBase
|
||||
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin,
|
||||
PSIDetectorBase,
|
||||
)
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
from ophyd_devices.sim.sim_data import SimulatedDataMonitor
|
||||
from ophyd_devices.sim.sim_signals import ReadOnlySignal, SetableSignal
|
||||
from ophyd_devices.utils.errors import DeviceStopError
|
||||
from ophyd_devices.utils import bec_utils
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@ -26,18 +20,25 @@ class SimMonitor(ReadOnlySignal):
|
||||
A simulated device mimic any 1D Axis (position, temperature, beam).
|
||||
|
||||
It's readback is a computed signal, which is configurable by the user and from the command line.
|
||||
The corresponding simulation class is sim_cls=SimulatedDataMonitor, more details on defaults within the simulation class.
|
||||
The corresponding simulation class is sim_cls=SimulatedDataMonitor, more details on defaults
|
||||
within the simulation class.
|
||||
|
||||
>>> monitor = SimMonitor(name="monitor")
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name (string) : Name of the device. This is the only required argmuent, passed on to all signals of the device.
|
||||
precision (integer) : Precision of the readback in digits, written to .describe(). Default is 3 digits.
|
||||
sim_init (dict) : Dictionary to initiate parameters of the simulation, check simulation type defaults for more details.
|
||||
parent : Parent device, optional, is used internally if this signal/device is part of a larger device.
|
||||
kind : A member the Kind IntEnum (or equivalent integer), optional. Default is Kind.normal. See Kind for options.
|
||||
device_manager : DeviceManager from BEC, optional . Within startup of simulation, device_manager is passed on automatically.
|
||||
name (string) : Name of the device. This is the only required argmuent,
|
||||
passed on to all signals of the device.
|
||||
precision (integer) : Precision of the readback in digits, written to .describe().
|
||||
Default is 3 digits.
|
||||
sim_init (dict) : Dictionary to initiate parameters of the simulation,
|
||||
check simulation type defaults for more details.
|
||||
parent : Parent device, optional, is used internally if this
|
||||
signal/device is part of a larger device.
|
||||
kind : A member the Kind IntEnum (or equivalent integer), optional.
|
||||
Default is Kind.normal. See Kind for options.
|
||||
device_manager : DeviceManager from BEC, optional . Within startup of simulation,
|
||||
device_manager is passed on automatically.
|
||||
|
||||
"""
|
||||
|
||||
@ -81,135 +82,11 @@ class SimMonitor(ReadOnlySignal):
|
||||
return self._registered_proxies
|
||||
|
||||
|
||||
class SimMonitorAsyncPrepare(CustomDetectorMixin):
|
||||
"""Custom prepare for the SimMonitorAsync class."""
|
||||
|
||||
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
|
||||
super().__init__(*args, parent=parent, **kwargs)
|
||||
self._stream_ttl = 1800
|
||||
self._random_send_interval = None
|
||||
self._counter = 0
|
||||
self._thread_trigger = None
|
||||
self._thread_complete = None
|
||||
self.prep_random_interval()
|
||||
self.parent.current_trigger.subscribe(self._progress_update, run=False)
|
||||
|
||||
def clear_buffer(self):
|
||||
"""Clear the data buffer."""
|
||||
self.parent.data_buffer["value"].clear()
|
||||
self.parent.data_buffer["timestamp"].clear()
|
||||
|
||||
def prep_random_interval(self):
|
||||
"""Prepare counter and random interval to send data to BEC."""
|
||||
self._random_send_interval = np.random.randint(1, 10)
|
||||
self.parent.current_trigger.set(0).wait()
|
||||
self._counter = self.parent.current_trigger.get()
|
||||
|
||||
def on_stage(self):
|
||||
"""Prepare the device for staging."""
|
||||
self.clear_buffer()
|
||||
self.prep_random_interval()
|
||||
|
||||
def on_complete(self):
|
||||
"""Prepare the device for completion."""
|
||||
status = DeviceStatus(self.parent)
|
||||
|
||||
def on_complete_call(status: DeviceStatus) -> None:
|
||||
try:
|
||||
if self.parent.data_buffer["value"]:
|
||||
self._send_data_to_bec()
|
||||
if self.parent.stopped:
|
||||
raise DeviceStopError(f"{self.parent.name} was stopped")
|
||||
status.set_finished()
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
content = traceback.format_exc()
|
||||
status.set_exception(exc=exc)
|
||||
logger.warning(f"Error in {self.parent.name} on_complete; Traceback: {content}")
|
||||
|
||||
self._thread_complete = Thread(target=on_complete_call, args=(status,))
|
||||
self._thread_complete.start()
|
||||
return status
|
||||
|
||||
def _send_data_to_bec(self) -> None:
|
||||
"""Sends bundled data to BEC"""
|
||||
if self.parent.scaninfo.scan_msg is None:
|
||||
return
|
||||
metadata = self.parent.scaninfo.scan_msg.metadata
|
||||
metadata.update({"async_update": self.parent.async_update.get()})
|
||||
|
||||
msg = messages.DeviceMessage(
|
||||
signals={self.parent.readback.name: self.parent.data_buffer},
|
||||
metadata=self.parent.scaninfo.scan_msg.metadata,
|
||||
)
|
||||
self.parent.connector.xadd(
|
||||
MessageEndpoints.device_async_readback(
|
||||
scan_id=self.parent.scaninfo.scan_id, device=self.parent.name
|
||||
),
|
||||
{"data": msg},
|
||||
expire=self._stream_ttl,
|
||||
)
|
||||
self.clear_buffer()
|
||||
|
||||
def on_trigger(self):
|
||||
"""Prepare the device for triggering."""
|
||||
status = DeviceStatus(self.parent)
|
||||
|
||||
def on_trigger_call(status: DeviceStatus) -> None:
|
||||
try:
|
||||
self.parent.data_buffer["value"].append(self.parent.readback.get())
|
||||
self.parent.data_buffer["timestamp"].append(self.parent.readback.timestamp)
|
||||
self._counter += 1
|
||||
self.parent.current_trigger.set(self._counter).wait()
|
||||
if self._counter % self._random_send_interval == 0:
|
||||
self._send_data_to_bec()
|
||||
if self.parent.stopped:
|
||||
raise DeviceStopError(f"{self.parent.name} was stopped")
|
||||
status.set_finished()
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
content = traceback.format_exc()
|
||||
logger.warning(
|
||||
f"Error in on_trigger_call in device {self.parent.name}; Traceback: {content}"
|
||||
)
|
||||
status.set_exception(exc=exc)
|
||||
|
||||
self._thread_trigger = Thread(target=on_trigger_call, args=(status,))
|
||||
self._thread_trigger.start()
|
||||
return status
|
||||
|
||||
def _progress_update(self, value: int, **kwargs):
|
||||
"""Update the progress of the device."""
|
||||
max_value = self.parent.scaninfo.num_points
|
||||
# pylint: disable=protected-access
|
||||
self.parent._run_subs(
|
||||
sub_type=self.parent.SUB_PROGRESS,
|
||||
value=value,
|
||||
max_value=max_value,
|
||||
done=bool(max_value == value),
|
||||
)
|
||||
|
||||
def on_stop(self):
|
||||
"""Stop the device."""
|
||||
if self._thread_trigger:
|
||||
self._thread_trigger.join()
|
||||
if self._thread_complete:
|
||||
self._thread_complete.join()
|
||||
self._thread_trigger = None
|
||||
self._thread_complete = None
|
||||
|
||||
|
||||
class SimMonitorAsync(PSIDetectorBase):
|
||||
"""
|
||||
A simulated device to mimic the behaviour of an asynchronous monitor.
|
||||
|
||||
During a scan, this device will send data not in sync with the point ID to BEC,
|
||||
but buffer data and send it in random intervals.s
|
||||
"""
|
||||
class SimMonitorAsyncControl(Device):
|
||||
"""SimMonitor Sync Control Device"""
|
||||
|
||||
USER_ACCESS = ["sim", "registered_proxies", "async_update"]
|
||||
|
||||
custom_prepare_cls = SimMonitorAsyncPrepare
|
||||
sim_cls = SimulatedDataMonitor
|
||||
BIT_DEPTH = np.uint32
|
||||
|
||||
@ -221,17 +98,17 @@ class SimMonitorAsync(PSIDetectorBase):
|
||||
SUB_PROGRESS = "progress"
|
||||
_default_sub = SUB_READBACK
|
||||
|
||||
def __init__(
|
||||
self, name, *, sim_init: dict = None, parent=None, kind=None, device_manager=None, **kwargs
|
||||
):
|
||||
def __init__(self, name, *, sim_init: dict = None, parent=None, device_manager=None, **kwargs):
|
||||
if device_manager:
|
||||
self.device_manager = device_manager
|
||||
else:
|
||||
self.device_manager = bec_utils.DMMock()
|
||||
self.connector = self.device_manager.connector
|
||||
self.sim_init = sim_init
|
||||
self.device_manager = device_manager
|
||||
self.sim = self.sim_cls(parent=self, **kwargs)
|
||||
self._registered_proxies = {}
|
||||
|
||||
super().__init__(
|
||||
name=name, parent=parent, kind=kind, device_manager=device_manager, **kwargs
|
||||
)
|
||||
super().__init__(name=name, parent=parent, **kwargs)
|
||||
self.sim.sim_state[self.name] = self.sim.sim_state.pop(self.readback.name, None)
|
||||
self.readback.name = self.name
|
||||
self._data_buffer = {"value": [], "timestamp": []}
|
||||
@ -247,3 +124,101 @@ class SimMonitorAsync(PSIDetectorBase):
|
||||
def registered_proxies(self) -> None:
|
||||
"""Dictionary of registered signal_names and proxies."""
|
||||
return self._registered_proxies
|
||||
|
||||
|
||||
class SimMonitorAsync(PSIDeviceBase, SimMonitorAsyncControl):
|
||||
"""
|
||||
A simulated device to mimic the behaviour of an asynchronous monitor.
|
||||
|
||||
During a scan, this device will send data not in sync with the point ID to BEC,
|
||||
but buffer data and send it in random intervals.s
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, name: str, scan_info=None, parent: Device = None, device_manager=None, **kwargs
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name, scan_info=scan_info, parent=parent, device_manager=device_manager, **kwargs
|
||||
)
|
||||
self._stream_ttl = 1800
|
||||
self._random_send_interval = None
|
||||
self._counter = 0
|
||||
self.prep_random_interval()
|
||||
|
||||
def on_connected(self):
|
||||
self.current_trigger.subscribe(self._progress_update, run=False)
|
||||
|
||||
def clear_buffer(self):
|
||||
"""Clear the data buffer."""
|
||||
self.data_buffer["value"].clear()
|
||||
self.data_buffer["timestamp"].clear()
|
||||
|
||||
def prep_random_interval(self):
|
||||
"""Prepare counter and random interval to send data to BEC."""
|
||||
self._random_send_interval = np.random.randint(1, 10)
|
||||
self.current_trigger.set(0).wait()
|
||||
self._counter = self.current_trigger.get()
|
||||
|
||||
def on_stage(self):
|
||||
"""Prepare the device for staging."""
|
||||
self.clear_buffer()
|
||||
self.prep_random_interval()
|
||||
|
||||
def on_complete(self) -> StatusBase:
|
||||
"""Prepare the device for completion."""
|
||||
|
||||
def complete_action():
|
||||
if self.data_buffer["value"]:
|
||||
self._send_data_to_bec()
|
||||
|
||||
status = self.task_handler.submit_task(complete_action)
|
||||
return status
|
||||
|
||||
def _send_data_to_bec(self) -> None:
|
||||
"""Sends bundled data to BEC"""
|
||||
if self.scan_info.msg is None:
|
||||
return
|
||||
metadata = self.scan_info.msg.metadata
|
||||
metadata.update({"async_update": self.async_update.get()})
|
||||
|
||||
msg = messages.DeviceMessage(
|
||||
signals={self.readback.name: self.data_buffer}, metadata=self.scan_info.msg.metadata
|
||||
)
|
||||
self.connector.xadd(
|
||||
MessageEndpoints.device_async_readback(
|
||||
scan_id=self.scan_info.msg.scan_id, device=self.name
|
||||
),
|
||||
{"data": msg},
|
||||
expire=self._stream_ttl,
|
||||
)
|
||||
self.clear_buffer()
|
||||
|
||||
def on_trigger(self):
|
||||
"""Prepare the device for triggering."""
|
||||
|
||||
def trigger_action():
|
||||
"""Trigger actions"""
|
||||
self.data_buffer["value"].append(self.readback.get())
|
||||
self.data_buffer["timestamp"].append(self.readback.timestamp)
|
||||
self._counter += 1
|
||||
self.current_trigger.set(self._counter).wait()
|
||||
if self._counter % self._random_send_interval == 0:
|
||||
self._send_data_to_bec()
|
||||
|
||||
status = self.task_handler.submit_task(trigger_action)
|
||||
return status
|
||||
|
||||
def _progress_update(self, value: int, **kwargs):
|
||||
"""Update the progress of the device."""
|
||||
max_value = self.scan_info.msg.num_points
|
||||
# pylint: disable=protected-access
|
||||
self._run_subs(
|
||||
sub_type=self.SUB_PROGRESS,
|
||||
value=value,
|
||||
max_value=max_value,
|
||||
done=bool(max_value == value),
|
||||
)
|
||||
|
||||
def on_stop(self):
|
||||
"""Stop the device."""
|
||||
self.task_handler.shutdown()
|
||||
|
@ -15,7 +15,6 @@ from ophyd import Device, DeviceStatus, Kind, Staged
|
||||
from ophyd_devices.sim.sim_data import SimulatedDataWaveform
|
||||
from ophyd_devices.sim.sim_signals import ReadOnlySignal, SetableSignal
|
||||
from ophyd_devices.utils import bec_utils
|
||||
from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin
|
||||
from ophyd_devices.utils.errors import DeviceStopError
|
||||
|
||||
logger = bec_logger.logger
|
||||
@ -67,7 +66,15 @@ class SimWaveform(Device):
|
||||
async_update = Cpt(SetableSignal, value="append", kind=Kind.config)
|
||||
|
||||
def __init__(
|
||||
self, name, *, kind=None, parent=None, sim_init: dict = None, device_manager=None, **kwargs
|
||||
self,
|
||||
name,
|
||||
*,
|
||||
kind=None,
|
||||
parent=None,
|
||||
sim_init: dict = None,
|
||||
device_manager=None,
|
||||
scan_info=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.sim_init = sim_init
|
||||
self._registered_proxies = {}
|
||||
@ -83,9 +90,8 @@ class SimWaveform(Device):
|
||||
self._stream_ttl = 1800 # 30 min max
|
||||
self.stopped = False
|
||||
self._staged = Staged.no
|
||||
self.scaninfo = None
|
||||
self._trigger_thread = None
|
||||
self._update_scaninfo()
|
||||
self.scan_info = scan_info
|
||||
if self.sim_init:
|
||||
self.sim.set_init(self.sim_init)
|
||||
|
||||
@ -124,7 +130,7 @@ class SimWaveform(Device):
|
||||
|
||||
def _send_async_update(self):
|
||||
"""Send the async update to BEC."""
|
||||
metadata = self.scaninfo.scan_msg.metadata
|
||||
metadata = self.scan_info.msg.metadata
|
||||
async_update_type = self.async_update.get()
|
||||
if async_update_type not in ["extend", "append"]:
|
||||
raise ValueError(f"Invalid async_update type: {async_update_type}")
|
||||
@ -134,19 +140,15 @@ class SimWaveform(Device):
|
||||
signals={self.waveform.name: {"value": self.waveform.get(), "timestamp": time.time()}},
|
||||
metadata=metadata,
|
||||
)
|
||||
# logger.warning(f"Adding async update to {self.name} and {self.scaninfo.scan_id}")
|
||||
# logger.warning(f"Adding async update to {self.name} and {self.scan_info.msg.scan_id}")
|
||||
self.connector.xadd(
|
||||
MessageEndpoints.device_async_readback(scan_id=self.scaninfo.scan_id, device=self.name),
|
||||
MessageEndpoints.device_async_readback(
|
||||
scan_id=self.scan_info.msg.scan_id, device=self.name
|
||||
),
|
||||
{"data": msg},
|
||||
expire=self._stream_ttl,
|
||||
)
|
||||
|
||||
def _update_scaninfo(self) -> None:
|
||||
"""Update scaninfo from BecScaninfoMixing
|
||||
This depends on device manager and operation/sim_mode
|
||||
"""
|
||||
self.scaninfo = BecScaninfoMixin(self.device_manager)
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
"""Stage the camera for upcoming scan
|
||||
|
||||
@ -160,17 +162,18 @@ class SimWaveform(Device):
|
||||
if self._staged is Staged.yes:
|
||||
|
||||
return super().stage()
|
||||
self.scaninfo.load_scan_metadata()
|
||||
self.file_path.set(
|
||||
os.path.join(
|
||||
self.file_path.get(), self.file_pattern.get().format(self.scaninfo.scan_number)
|
||||
self.file_path.get(), self.file_pattern.get().format(self.scan_info.msg.scan_number)
|
||||
)
|
||||
)
|
||||
self.frames.set(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)
|
||||
self.exp_time.set(self.scaninfo.exp_time)
|
||||
self.burst.set(self.scaninfo.frames_per_trigger)
|
||||
self.frames.set(
|
||||
self.scan_info.msg.num_points * self.scan_info.msg.scan_parameters["frames_per_trigger"]
|
||||
)
|
||||
self.exp_time.set(self.scan_info.msg.scan_parameters["exp_time"])
|
||||
self.burst.set(self.scan_info.msg.scan_parameters["frames_per_trigger"])
|
||||
self.stopped = False
|
||||
logger.warning(f"Staged {self.name}, scan_id : {self.scaninfo.scan_id}")
|
||||
logger.warning(f"Staged {self.name}, scan_id : {self.scan_info.msg.scan_id}")
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
|
@ -1,5 +1,21 @@
|
||||
""" Utilities to mock and test devices."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest import mock
|
||||
|
||||
from bec_lib.devicemanager import ScanInfo
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.utils.import_utils import lazy_import_from
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.messages import ScanStatusMessage
|
||||
else:
|
||||
# TODO: put back normal import when Pydantic gets faster
|
||||
ScanStatusMessage = lazy_import_from("bec_lib.messages", ("ScanStatusMessage",))
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
def patch_dual_pvs(device):
|
||||
"""Patch dual PVs"""
|
||||
@ -255,3 +271,128 @@ class MockPV:
|
||||
use_monitor=use_monitor,
|
||||
)
|
||||
return data["value"] if data is not None else None
|
||||
|
||||
|
||||
def get_mock_scan_info():
|
||||
"""
|
||||
Get a mock scan info object.
|
||||
"""
|
||||
return ScanInfo(msg=fake_scan_status_msg())
|
||||
|
||||
|
||||
def fake_scan_status_msg():
|
||||
"""
|
||||
Create a fake scan status message.
|
||||
"""
|
||||
logger.warning(
|
||||
("Device is not connected to a Redis server. Fetching mocked ScanStatusMessage.")
|
||||
)
|
||||
return ScanStatusMessage(
|
||||
metadata={},
|
||||
scan_id="mock_scan_id",
|
||||
status="closed",
|
||||
scan_number=0,
|
||||
session_id=None,
|
||||
num_points=11,
|
||||
scan_name="mock_line_scan",
|
||||
scan_type="step",
|
||||
dataset_number=0,
|
||||
scan_report_devices=["samx"],
|
||||
user_metadata={},
|
||||
readout_priority={
|
||||
"monitored": ["bpm4a", "samx"],
|
||||
"baseline": ["eyex"],
|
||||
"async": ["waveform"],
|
||||
"continuous": [],
|
||||
"on_request": ["flyer_sim"],
|
||||
},
|
||||
scan_parameters={
|
||||
"exp_time": 0,
|
||||
"frames_per_trigger": 1,
|
||||
"settling_time": 0,
|
||||
"readout_time": 0,
|
||||
"optim_trajectory": None,
|
||||
"return_to_start": True,
|
||||
"relative": True,
|
||||
"system_config": {"file_suffix": None, "file_directory": None},
|
||||
},
|
||||
request_inputs={
|
||||
"arg_bundle": ["samx", -10, 10],
|
||||
"inputs": {},
|
||||
"kwargs": {
|
||||
"steps": 11,
|
||||
"relative": True,
|
||||
"system_config": {"file_suffix": None, "file_directory": None},
|
||||
},
|
||||
},
|
||||
info={
|
||||
"readout_priority": {
|
||||
"monitored": ["bpm4a", "samx"],
|
||||
"baseline": ["eyex"],
|
||||
"async": ["waveform"],
|
||||
"continuous": [],
|
||||
"on_request": ["flyer_sim"],
|
||||
},
|
||||
"file_suffix": None,
|
||||
"file_directory": None,
|
||||
"user_metadata": {},
|
||||
"RID": "a1d86f61-191c-4460-bcd6-f33c61b395ea",
|
||||
"scan_id": "3edb8219-75a7-4791-8f86-d5ca112b771a",
|
||||
"queue_id": "0f3639ee-899f-4ad1-9e71-f40514c937ef",
|
||||
"scan_motors": ["samx"],
|
||||
"num_points": 11,
|
||||
"positions": [
|
||||
[-10.0],
|
||||
[-8.0],
|
||||
[-6.0],
|
||||
[-4.0],
|
||||
[-2.0],
|
||||
[0.0],
|
||||
[2.0],
|
||||
[4.0],
|
||||
[6.0],
|
||||
[8.0],
|
||||
[10.0],
|
||||
],
|
||||
"file_path": "./data/test_file",
|
||||
"scan_name": "mock_line_scan",
|
||||
"scan_type": "step",
|
||||
"scan_number": 0,
|
||||
"dataset_number": 0,
|
||||
"exp_time": 0,
|
||||
"frames_per_trigger": 1,
|
||||
"settling_time": 0,
|
||||
"readout_time": 0,
|
||||
"scan_report_devices": ["samx"],
|
||||
"monitor_sync": "bec",
|
||||
"scan_parameters": {
|
||||
"exp_time": 0,
|
||||
"frames_per_trigger": 1,
|
||||
"settling_time": 0,
|
||||
"readout_time": 0,
|
||||
"optim_trajectory": None,
|
||||
"return_to_start": True,
|
||||
"relative": True,
|
||||
"system_config": {"file_suffix": None, "file_directory": None},
|
||||
},
|
||||
"request_inputs": {
|
||||
"arg_bundle": ["samx", -10, 10],
|
||||
"inputs": {},
|
||||
"kwargs": {
|
||||
"steps": 11,
|
||||
"relative": True,
|
||||
"system_config": {"file_suffix": None, "file_directory": None},
|
||||
},
|
||||
},
|
||||
"scan_msgs": [
|
||||
"metadata={'file_suffix': None, 'file_directory': None, 'user_metadata': {}, 'RID': 'a1d86f61-191c-4460-bcd6-f33c61b395ea'} scan_type='mock_line_scan' parameter={'args': {'samx': [-10, 10]}, 'kwargs': {'steps': 11, 'relative': True, 'system_config': {'file_suffix': None, 'file_directory': None}}} queue='primary'"
|
||||
],
|
||||
"args": {"samx": [-10, 10]},
|
||||
"kwargs": {
|
||||
"steps": 11,
|
||||
"relative": True,
|
||||
"system_config": {"file_suffix": None, "file_directory": None},
|
||||
},
|
||||
},
|
||||
timestamp=1737100681.694211,
|
||||
)
|
||||
|
@ -1,142 +0,0 @@
|
||||
import getpass
|
||||
|
||||
from bec_lib import bec_logger, messages
|
||||
from bec_lib.devicemanager import DeviceManagerBase
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
|
||||
from ophyd_devices.utils.bec_utils import DMMock
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class BECInfoMsgMock:
|
||||
"""Mock BECInfoMsg class
|
||||
|
||||
This class is used for mocking BECInfoMsg for testing purposes
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mockrid: str = "mockrid1111",
|
||||
mockqueueid: str = "mockqueue_id111",
|
||||
scan_number: int = 1,
|
||||
exp_time: float = 15e-3,
|
||||
num_points: int = 500,
|
||||
readout_time: float = 3e-3,
|
||||
scan_type: str = "fly",
|
||||
num_lines: int = 1,
|
||||
frames_per_trigger: int = 1,
|
||||
) -> None:
|
||||
self.mockrid = mockrid
|
||||
self.mockqueueid = mockqueueid
|
||||
self.scan_number = scan_number
|
||||
self.exp_time = exp_time
|
||||
self.num_points = num_points
|
||||
self.readout_time = readout_time
|
||||
self.scan_type = scan_type
|
||||
self.num_lines = num_lines
|
||||
self.frames_per_trigger = frames_per_trigger
|
||||
|
||||
def get_bec_info_msg(self) -> dict:
|
||||
"""Get BECInfoMsg object"""
|
||||
info_msg = {
|
||||
"RID": self.mockrid,
|
||||
"queue_id": self.mockqueueid,
|
||||
"scan_number": self.scan_number,
|
||||
"exp_time": self.exp_time,
|
||||
"num_points": self.num_points,
|
||||
"readout_time": self.readout_time,
|
||||
"scan_type": self.scan_type,
|
||||
"num_lines": self.exp_time,
|
||||
"frames_per_trigger": self.frames_per_trigger,
|
||||
}
|
||||
|
||||
return info_msg
|
||||
|
||||
|
||||
class BecScaninfoMixin:
|
||||
"""BecScaninfoMixin class
|
||||
|
||||
Args:
|
||||
device_manager (DeviceManagerBase): DeviceManagerBase object
|
||||
sim_mode (bool): Simulation mode flag
|
||||
bec_info_msg (dict): BECInfoMsg object
|
||||
Returns:
|
||||
BecScaninfoMixin: BecScaninfoMixin object
|
||||
"""
|
||||
|
||||
def __init__(self, device_manager: DeviceManagerBase = None, bec_info_msg=None) -> None:
|
||||
self.sim_mode = bool(isinstance(device_manager, DMMock))
|
||||
self.device_manager = device_manager
|
||||
self.scan_msg = None
|
||||
self.scan_id = None
|
||||
if bec_info_msg is None:
|
||||
infomsgmock = BECInfoMsgMock()
|
||||
self.bec_info_msg = infomsgmock.get_bec_info_msg()
|
||||
else:
|
||||
self.bec_info_msg = bec_info_msg
|
||||
|
||||
self.metadata = None
|
||||
self.scan_id = None
|
||||
self.scan_number = None
|
||||
self.exp_time = None
|
||||
self.frames_per_trigger = None
|
||||
self.num_points = None
|
||||
self.scan_type = None
|
||||
|
||||
def get_bec_info_msg(self) -> None:
|
||||
"""Get BECInfoMsg object"""
|
||||
return self.bec_info_msg
|
||||
|
||||
def change_config(self, bec_info_msg: dict) -> None:
|
||||
"""Change BECInfoMsg object"""
|
||||
self.bec_info_msg = bec_info_msg
|
||||
|
||||
def _get_current_scan_msg(self) -> messages.ScanStatusMessage:
|
||||
"""Get current scan message
|
||||
|
||||
Returns:
|
||||
messages.ScanStatusMessage: messages.ScanStatusMessage object
|
||||
"""
|
||||
if not self.sim_mode:
|
||||
msg = self.device_manager.connector.get(MessageEndpoints.scan_status())
|
||||
if not isinstance(msg, messages.ScanStatusMessage):
|
||||
return None
|
||||
return msg
|
||||
|
||||
return messages.ScanStatusMessage(scan_id="1", status="open", info=self.bec_info_msg)
|
||||
|
||||
def get_username(self) -> str:
|
||||
"""Get username"""
|
||||
if self.sim_mode:
|
||||
return getpass.getuser()
|
||||
|
||||
msg = self.device_manager.connector.get(MessageEndpoints.account())
|
||||
if msg:
|
||||
return msg
|
||||
return getpass.getuser()
|
||||
|
||||
def load_scan_metadata(self) -> None:
|
||||
"""Load scan metadata
|
||||
|
||||
This function loads scan metadata from the current scan message
|
||||
"""
|
||||
self.scan_msg = scan_msg = self._get_current_scan_msg()
|
||||
try:
|
||||
logger.info(f"Received scan msg for {self.scan_msg.content['scan_id']}")
|
||||
self.metadata = {
|
||||
"scan_id": scan_msg.content["scan_id"],
|
||||
"RID": scan_msg.content["info"]["RID"],
|
||||
"queue_id": scan_msg.content["info"]["queue_id"],
|
||||
}
|
||||
self.scan_id = scan_msg.content["scan_id"]
|
||||
self.scan_number = scan_msg.content["info"]["scan_number"]
|
||||
self.exp_time = scan_msg.content["info"]["exp_time"]
|
||||
self.frames_per_trigger = scan_msg.content["info"]["frames_per_trigger"]
|
||||
self.num_points = scan_msg.content["info"]["num_points"]
|
||||
self.scan_type = scan_msg.content["info"].get("scan_type", "step")
|
||||
self.readout_time = scan_msg.content["info"]["readout_time"]
|
||||
except Exception as exc:
|
||||
logger.error(f"Failed to load scan metadata: {exc}.")
|
||||
|
||||
self.username = self.get_username()
|
@ -1,3 +1,5 @@
|
||||
""" Utility class linked to BEC"""
|
||||
|
||||
import time
|
||||
|
||||
from bec_lib import bec_logger
|
||||
@ -11,8 +13,9 @@ logger = bec_logger.logger
|
||||
DEFAULT_EPICSSIGNAL_VALUE = object()
|
||||
|
||||
|
||||
# TODO maybe specify here that this DeviceMock is for usage in the DeviceServer
|
||||
class DeviceMock:
|
||||
"""Mock for Device"""
|
||||
|
||||
def __init__(self, name: str, value: float = 0.0):
|
||||
self.name = name
|
||||
self.read_buffer = value
|
||||
@ -21,13 +24,16 @@ class DeviceMock:
|
||||
self._enabled = True
|
||||
|
||||
def read(self):
|
||||
"""Return the current value of the device"""
|
||||
return {self.name: {"value": self.read_buffer}}
|
||||
|
||||
def readback(self):
|
||||
"""Return the current value of the device"""
|
||||
return self.read_buffer
|
||||
|
||||
@property
|
||||
def read_only(self) -> bool:
|
||||
"""Get the read only status of the device"""
|
||||
return self._read_only
|
||||
|
||||
@read_only.setter
|
||||
@ -36,6 +42,7 @@ class DeviceMock:
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""Get the enabled status of the device"""
|
||||
return self._enabled
|
||||
|
||||
@enabled.setter
|
||||
@ -44,10 +51,12 @@ class DeviceMock:
|
||||
|
||||
@property
|
||||
def user_parameter(self):
|
||||
"""Get the user parameter of the device"""
|
||||
return self._config["userParameter"]
|
||||
|
||||
@property
|
||||
def obj(self):
|
||||
"""Get the device object"""
|
||||
return self
|
||||
|
||||
|
||||
|
193
ophyd_devices/utils/psi_device_base_utils.py
Normal file
193
ophyd_devices/utils/psi_device_base_utils.py
Normal file
@ -0,0 +1,193 @@
|
||||
""" Utility handler to run tasks (function, conditions) in an asynchronous fashion."""
|
||||
|
||||
import ctypes
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.file_utils import get_full_file_path
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.utils.import_utils import lazy_import_from
|
||||
from ophyd import Device, DeviceStatus, StatusBase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.messages import ScanStatusMessage
|
||||
else:
|
||||
# TODO: put back normal import when Pydantic gets faster
|
||||
ScanStatusMessage = lazy_import_from("bec_lib.messages", ("ScanStatusMessage",))
|
||||
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
set_async_exc = ctypes.pythonapi.PyThreadState_SetAsyncExc
|
||||
|
||||
|
||||
class TaskState(str, Enum):
|
||||
"""Possible task states"""
|
||||
|
||||
NOT_STARTED = "not_started"
|
||||
RUNNING = "running"
|
||||
TIMEOUT = "timeout"
|
||||
ERROR = "error"
|
||||
COMPLETED = "completed"
|
||||
KILLED = "killed"
|
||||
|
||||
|
||||
class TaskKilledError(Exception):
|
||||
"""Exception raised when a task thread is killed"""
|
||||
|
||||
|
||||
class TaskStatus(DeviceStatus):
|
||||
"""Thin wrapper around StatusBase to add information about tasks"""
|
||||
|
||||
def __init__(self, device: Device, *, timeout=None, settle_time=0, done=None, success=None):
|
||||
super().__init__(
|
||||
device=device, timeout=timeout, settle_time=settle_time, done=done, success=success
|
||||
)
|
||||
self._state = TaskState.NOT_STARTED
|
||||
self._task_id = str(uuid.uuid4())
|
||||
|
||||
@property
|
||||
def exc(self) -> Exception:
|
||||
"""Get the exception of the task"""
|
||||
return self.exception()
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the task"""
|
||||
return self._state.value
|
||||
|
||||
@state.setter
|
||||
def state(self, value: TaskState):
|
||||
self._state = value
|
||||
|
||||
@property
|
||||
def task_id(self) -> bool:
|
||||
"""Get the task ID"""
|
||||
return self._task_id
|
||||
|
||||
|
||||
class TaskHandler:
|
||||
"""Handler to manage asynchronous tasks"""
|
||||
|
||||
def __init__(self, parent: Device):
|
||||
"""Initialize the handler"""
|
||||
self._tasks = {}
|
||||
self._parent = parent
|
||||
|
||||
def submit_task(self, task: callable, run: bool = True) -> TaskStatus:
|
||||
"""Submit a task to the task handler.
|
||||
|
||||
Args:
|
||||
task: The task to run.
|
||||
run: Whether to run the task immediately.
|
||||
"""
|
||||
task_status = TaskStatus(device=self._parent)
|
||||
thread = threading.Thread(
|
||||
target=self._wrap_task,
|
||||
args=(task, task_status),
|
||||
name=f"task {task_status.task_id}",
|
||||
daemon=True,
|
||||
)
|
||||
self._tasks.update({task_status.task_id: (task_status, thread)})
|
||||
if run is True:
|
||||
self.start_task(task_status)
|
||||
return task_status
|
||||
|
||||
def start_task(self, task_status: TaskStatus) -> None:
|
||||
"""Start a task,
|
||||
|
||||
Args:
|
||||
task_status: The task status object.
|
||||
"""
|
||||
thread = self._tasks[task_status.task_id][1]
|
||||
if thread.is_alive():
|
||||
logger.warning(f"Task with ID {task_status.task_id} is already running.")
|
||||
return
|
||||
thread.start()
|
||||
task_status.state = TaskState.RUNNING
|
||||
|
||||
def _wrap_task(self, task: callable, task_status: TaskStatus):
|
||||
"""Wrap the task in a function"""
|
||||
try:
|
||||
task()
|
||||
except TimeoutError as exc:
|
||||
content = traceback.format_exc()
|
||||
logger.warning(
|
||||
(
|
||||
f"Timeout Exception in task handler for task {task_status.task_id},"
|
||||
f" Traceback: {content}"
|
||||
)
|
||||
)
|
||||
task_status.set_exception(exc)
|
||||
task_status.state = TaskState.TIMEOUT
|
||||
except TaskKilledError as exc:
|
||||
exc = exc.__class__(
|
||||
f"Task {task_status.task_id} was killed. ThreadID:"
|
||||
f" {self._tasks[task_status.task_id][1].ident}"
|
||||
)
|
||||
content = traceback.format_exc()
|
||||
logger.warning(
|
||||
(
|
||||
f"TaskKilled Exception in task handler for task {task_status.task_id},"
|
||||
f" Traceback: {content}"
|
||||
)
|
||||
)
|
||||
task_status.set_exception(exc)
|
||||
task_status.state = TaskState.KILLED
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
content = traceback.format_exc()
|
||||
logger.warning(
|
||||
f"Exception in task handler for task {task_status.task_id}, Traceback: {content}"
|
||||
)
|
||||
task_status.set_exception(exc)
|
||||
task_status.state = TaskState.ERROR
|
||||
else:
|
||||
task_status.set_finished()
|
||||
task_status.state = TaskState.COMPLETED
|
||||
finally:
|
||||
self._tasks.pop(task_status.task_id)
|
||||
|
||||
def kill_task(self, task_status: TaskStatus) -> None:
|
||||
"""Kill the thread
|
||||
|
||||
task_status: The task status object.
|
||||
"""
|
||||
thread = self._tasks[task_status.task_id][1]
|
||||
exception_cls = TaskKilledError
|
||||
|
||||
ident = ctypes.c_long(thread.ident)
|
||||
exc = ctypes.py_object(exception_cls)
|
||||
try:
|
||||
res = set_async_exc(ident, exc)
|
||||
if res == 0:
|
||||
raise ValueError("Invalid thread ID")
|
||||
elif res > 1:
|
||||
set_async_exc(ident, None)
|
||||
logger.warning(f"Exception raise while kille Thread {ident}; return value: {res}")
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logger.warning(f"Exception raised while killing thread {ident}: {e}")
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown all tasks of task handler"""
|
||||
for info in self._tasks.values():
|
||||
self.kill_task(info[0])
|
||||
self._tasks.clear()
|
||||
|
||||
|
||||
class FileHandler:
|
||||
"""Utility class for file operations."""
|
||||
|
||||
def get_file_path(
|
||||
self, scan_status_msg: ScanStatusMessage, name: str, create_dir: bool = True
|
||||
) -> str:
|
||||
"""Get the file path.
|
||||
|
||||
Args:
|
||||
scan_info_msg: The scan info message.
|
||||
name: The name of the file.
|
||||
create_dir: Whether to create the directory.
|
||||
"""
|
||||
return get_full_file_path(scan_status_msg=scan_status_msg, name=name, create_dir=create_dir)
|
@ -6,200 +6,215 @@ import pytest
|
||||
from ophyd import DeviceStatus, Staged
|
||||
from ophyd.utils.errors import RedundantStaging
|
||||
|
||||
from ophyd_devices.interfaces.base_classes.bec_device_base import BECDeviceBase, CustomPrepare
|
||||
from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
from ophyd_devices.utils.errors import DeviceStopError, DeviceTimeoutError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def detector_base():
|
||||
yield BECDeviceBase(name="test_detector")
|
||||
yield PSIDeviceBase(name="test_detector")
|
||||
|
||||
|
||||
def test_detector_base_init(detector_base):
|
||||
assert detector_base.stopped is False
|
||||
assert detector_base.name == "test_detector"
|
||||
assert "base_path" in detector_base.filewriter.service_config
|
||||
assert isinstance(detector_base.scaninfo, BecScaninfoMixin)
|
||||
assert issubclass(detector_base.custom_prepare_cls, CustomPrepare)
|
||||
assert detector_base.staged == Staged.no
|
||||
assert detector_base.destroyed == False
|
||||
|
||||
|
||||
def test_stage(detector_base):
|
||||
detector_base._staged = Staged.yes
|
||||
with pytest.raises(RedundantStaging):
|
||||
detector_base.stage()
|
||||
assert detector_base._staged == Staged.no
|
||||
assert detector_base.stopped is False
|
||||
detector_base._staged = Staged.no
|
||||
with (
|
||||
mock.patch.object(detector_base.custom_prepare, "on_stage") as mock_on_stage,
|
||||
mock.patch.object(detector_base.scaninfo, "load_scan_metadata") as mock_load_metadata,
|
||||
):
|
||||
with mock.patch.object(detector_base, "on_stage") as mock_on_stage:
|
||||
rtr = detector_base.stage()
|
||||
assert isinstance(rtr, list)
|
||||
mock_on_stage.assert_called_once()
|
||||
mock_load_metadata.assert_called_once()
|
||||
assert mock_on_stage.called is True
|
||||
with pytest.raises(RedundantStaging):
|
||||
detector_base.stage()
|
||||
detector_base._staged = Staged.no
|
||||
detector_base.stopped = True
|
||||
detector_base.stage()
|
||||
assert detector_base.stopped is False
|
||||
assert mock_on_stage.call_count == 2
|
||||
|
||||
|
||||
def test_pre_scan(detector_base):
|
||||
with mock.patch.object(detector_base.custom_prepare, "on_pre_scan") as mock_on_pre_scan:
|
||||
detector_base.pre_scan()
|
||||
mock_on_pre_scan.assert_called_once()
|
||||
# def test_stage(detector_base):
|
||||
# detector_base._staged = Staged.yes
|
||||
# with pytest.raises(RedundantStaging):
|
||||
# detector_base.stage()
|
||||
# assert detector_base.stopped is False
|
||||
# detector_base._staged = Staged.no
|
||||
# with (
|
||||
# mock.patch.object(detector_base.custom_prepare, "on_stage") as mock_on_stage,
|
||||
# mock.patch.object(detector_base.scaninfo, "load_scan_metadata") as mock_load_metadata,
|
||||
# ):
|
||||
# rtr = detector_base.stage()
|
||||
# assert isinstance(rtr, list)
|
||||
# mock_on_stage.assert_called_once()
|
||||
# mock_load_metadata.assert_called_once()
|
||||
# assert detector_base.stopped is False
|
||||
|
||||
|
||||
def test_trigger(detector_base):
|
||||
status = DeviceStatus(detector_base)
|
||||
with mock.patch.object(
|
||||
detector_base.custom_prepare, "on_trigger", side_effect=[None, status]
|
||||
) as mock_on_trigger:
|
||||
st = detector_base.trigger()
|
||||
assert isinstance(st, DeviceStatus)
|
||||
time.sleep(0.1)
|
||||
assert st.done is True
|
||||
st = detector_base.trigger()
|
||||
assert st.done is False
|
||||
assert id(st) == id(status)
|
||||
# def test_pre_scan(detector_base):
|
||||
# with mock.patch.object(detector_base.custom_prepare, "on_pre_scan") as mock_on_pre_scan:
|
||||
# detector_base.pre_scan()
|
||||
# mock_on_pre_scan.assert_called_once()
|
||||
|
||||
|
||||
def test_unstage(detector_base):
|
||||
detector_base.stopped = True
|
||||
with (
|
||||
mock.patch.object(detector_base.custom_prepare, "on_unstage") as mock_on_unstage,
|
||||
mock.patch.object(detector_base, "check_scan_id") as mock_check_scan_id,
|
||||
):
|
||||
rtr = detector_base.unstage()
|
||||
assert isinstance(rtr, list)
|
||||
assert mock_check_scan_id.call_count == 1
|
||||
assert mock_on_unstage.call_count == 1
|
||||
detector_base.stopped = False
|
||||
rtr = detector_base.unstage()
|
||||
assert isinstance(rtr, list)
|
||||
assert mock_check_scan_id.call_count == 2
|
||||
assert mock_on_unstage.call_count == 2
|
||||
# def test_trigger(detector_base):
|
||||
# status = DeviceStatus(detector_base)
|
||||
# with mock.patch.object(
|
||||
# detector_base.custom_prepare, "on_trigger", side_effect=[None, status]
|
||||
# ) as mock_on_trigger:
|
||||
# st = detector_base.trigger()
|
||||
# assert isinstance(st, DeviceStatus)
|
||||
# time.sleep(0.1)
|
||||
# assert st.done is True
|
||||
# st = detector_base.trigger()
|
||||
# assert st.done is False
|
||||
# assert id(st) == id(status)
|
||||
|
||||
|
||||
def test_complete(detector_base):
|
||||
status = DeviceStatus(detector_base)
|
||||
with mock.patch.object(
|
||||
detector_base.custom_prepare, "on_complete", side_effect=[None, status]
|
||||
) as mock_on_complete:
|
||||
st = detector_base.complete()
|
||||
assert isinstance(st, DeviceStatus)
|
||||
time.sleep(0.1)
|
||||
assert st.done is True
|
||||
st = detector_base.complete()
|
||||
assert st.done is False
|
||||
assert id(st) == id(status)
|
||||
# def test_unstage(detector_base):
|
||||
# detector_base.stopped = True
|
||||
# with (
|
||||
# mock.patch.object(detector_base.custom_prepare, "on_unstage") as mock_on_unstage,
|
||||
# mock.patch.object(detector_base, "check_scan_id") as mock_check_scan_id,
|
||||
# ):
|
||||
# rtr = detector_base.unstage()
|
||||
# assert isinstance(rtr, list)
|
||||
# assert mock_check_scan_id.call_count == 1
|
||||
# assert mock_on_unstage.call_count == 1
|
||||
# detector_base.stopped = False
|
||||
# rtr = detector_base.unstage()
|
||||
# assert isinstance(rtr, list)
|
||||
# assert mock_check_scan_id.call_count == 2
|
||||
# assert mock_on_unstage.call_count == 2
|
||||
|
||||
|
||||
def test_stop(detector_base):
|
||||
with mock.patch.object(detector_base.custom_prepare, "on_stop") as mock_on_stop:
|
||||
detector_base.stop()
|
||||
mock_on_stop.assert_called_once()
|
||||
assert detector_base.stopped is True
|
||||
# def test_complete(detector_base):
|
||||
# status = DeviceStatus(detector_base)
|
||||
# with mock.patch.object(
|
||||
# detector_base.custom_prepare, "on_complete", side_effect=[None, status]
|
||||
# ) as mock_on_complete:
|
||||
# st = detector_base.complete()
|
||||
# assert isinstance(st, DeviceStatus)
|
||||
# time.sleep(0.1)
|
||||
# assert st.done is True
|
||||
# st = detector_base.complete()
|
||||
# assert st.done is False
|
||||
# assert id(st) == id(status)
|
||||
|
||||
|
||||
def test_check_scan_id(detector_base):
|
||||
detector_base.scaninfo.scan_id = "abcde"
|
||||
detector_base.stopped = False
|
||||
detector_base.check_scan_id()
|
||||
assert detector_base.stopped is True
|
||||
detector_base.stopped = False
|
||||
detector_base.check_scan_id()
|
||||
assert detector_base.stopped is False
|
||||
# def test_stop(detector_base):
|
||||
# with mock.patch.object(detector_base.custom_prepare, "on_stop") as mock_on_stop:
|
||||
# detector_base.stop()
|
||||
# mock_on_stop.assert_called_once()
|
||||
# assert detector_base.stopped is True
|
||||
|
||||
|
||||
def test_wait_for_signal(detector_base):
|
||||
my_value = False
|
||||
|
||||
def my_callback():
|
||||
return my_value
|
||||
|
||||
detector_base
|
||||
status = detector_base.custom_prepare.wait_with_status(
|
||||
[(my_callback, True)],
|
||||
check_stopped=True,
|
||||
timeout=5,
|
||||
interval=0.01,
|
||||
exception_on_timeout=None,
|
||||
)
|
||||
time.sleep(0.1)
|
||||
assert status.done is False
|
||||
# Check first that it is stopped when detector_base.stop() is called
|
||||
detector_base.stop()
|
||||
# some delay to allow the stop to take effect
|
||||
time.sleep(0.15)
|
||||
assert status.done is True
|
||||
assert status.exception().args == DeviceStopError(f"{detector_base.name} was stopped").args
|
||||
detector_base.stopped = False
|
||||
status = detector_base.custom_prepare.wait_with_status(
|
||||
[(my_callback, True)],
|
||||
check_stopped=True,
|
||||
timeout=5,
|
||||
interval=0.01,
|
||||
exception_on_timeout=None,
|
||||
)
|
||||
# Check that thread resolves when expected value is set
|
||||
my_value = True
|
||||
# some delay to allow the stop to take effect
|
||||
time.sleep(0.15)
|
||||
assert status.done is True
|
||||
assert status.success is True
|
||||
assert status.exception() is None
|
||||
|
||||
detector_base.stopped = False
|
||||
# Check that wait for status runs into timeout with expectd exception
|
||||
my_value = "random_value"
|
||||
exception = TimeoutError("Timeout")
|
||||
status = detector_base.custom_prepare.wait_with_status(
|
||||
[(my_callback, True)],
|
||||
check_stopped=True,
|
||||
timeout=0.01,
|
||||
interval=0.01,
|
||||
exception_on_timeout=exception,
|
||||
)
|
||||
time.sleep(0.2)
|
||||
assert status.done is True
|
||||
assert id(status.exception()) == id(exception)
|
||||
assert status.success is False
|
||||
# def test_check_scan_id(detector_base):
|
||||
# detector_base.scaninfo.scan_id = "abcde"
|
||||
# detector_base.stopped = False
|
||||
# detector_base.check_scan_id()
|
||||
# assert detector_base.stopped is True
|
||||
# detector_base.stopped = False
|
||||
# detector_base.check_scan_id()
|
||||
# assert detector_base.stopped is False
|
||||
|
||||
|
||||
def test_wait_for_signal_returns_exception(detector_base):
|
||||
my_value = False
|
||||
# def test_wait_for_signal(detector_base):
|
||||
# my_value = False
|
||||
|
||||
def my_callback():
|
||||
return my_value
|
||||
# def my_callback():
|
||||
# return my_value
|
||||
|
||||
# Check that wait for status runs into timeout with expectd exception
|
||||
# detector_base
|
||||
# status = detector_base.custom_prepare.wait_with_status(
|
||||
# [(my_callback, True)],
|
||||
# check_stopped=True,
|
||||
# timeout=5,
|
||||
# interval=0.01,
|
||||
# exception_on_timeout=None,
|
||||
# )
|
||||
# time.sleep(0.1)
|
||||
# assert status.done is False
|
||||
# # Check first that it is stopped when detector_base.stop() is called
|
||||
# detector_base.stop()
|
||||
# # some delay to allow the stop to take effect
|
||||
# time.sleep(0.15)
|
||||
# assert status.done is True
|
||||
# assert status.exception().args == DeviceStopError(f"{detector_base.name} was stopped").args
|
||||
# detector_base.stopped = False
|
||||
# status = detector_base.custom_prepare.wait_with_status(
|
||||
# [(my_callback, True)],
|
||||
# check_stopped=True,
|
||||
# timeout=5,
|
||||
# interval=0.01,
|
||||
# exception_on_timeout=None,
|
||||
# )
|
||||
# # Check that thread resolves when expected value is set
|
||||
# my_value = True
|
||||
# # some delay to allow the stop to take effect
|
||||
# time.sleep(0.15)
|
||||
# assert status.done is True
|
||||
# assert status.success is True
|
||||
# assert status.exception() is None
|
||||
|
||||
exception = TimeoutError("Timeout")
|
||||
status = detector_base.custom_prepare.wait_with_status(
|
||||
[(my_callback, True)],
|
||||
check_stopped=True,
|
||||
timeout=0.01,
|
||||
interval=0.01,
|
||||
exception_on_timeout=exception,
|
||||
)
|
||||
time.sleep(0.2)
|
||||
assert status.done is True
|
||||
assert id(status.exception()) == id(exception)
|
||||
assert status.success is False
|
||||
# detector_base.stopped = False
|
||||
# # Check that wait for status runs into timeout with expectd exception
|
||||
# my_value = "random_value"
|
||||
# exception = TimeoutError("Timeout")
|
||||
# status = detector_base.custom_prepare.wait_with_status(
|
||||
# [(my_callback, True)],
|
||||
# check_stopped=True,
|
||||
# timeout=0.01,
|
||||
# interval=0.01,
|
||||
# exception_on_timeout=exception,
|
||||
# )
|
||||
# time.sleep(0.2)
|
||||
# assert status.done is True
|
||||
# assert id(status.exception()) == id(exception)
|
||||
# assert status.success is False
|
||||
|
||||
detector_base.stopped = False
|
||||
# Check that standard exception is thrown
|
||||
status = detector_base.custom_prepare.wait_with_status(
|
||||
[(my_callback, True)],
|
||||
check_stopped=True,
|
||||
timeout=0.01,
|
||||
interval=0.01,
|
||||
exception_on_timeout=None,
|
||||
)
|
||||
time.sleep(0.2)
|
||||
assert status.done is True
|
||||
assert (
|
||||
status.exception().args
|
||||
== DeviceTimeoutError(
|
||||
f"Timeout error for {detector_base.name} while waiting for signals {[(my_callback, True)]}"
|
||||
).args
|
||||
)
|
||||
assert status.success is False
|
||||
|
||||
# def test_wait_for_signal_returns_exception(detector_base):
|
||||
# my_value = False
|
||||
|
||||
# def my_callback():
|
||||
# return my_value
|
||||
|
||||
# # Check that wait for status runs into timeout with expectd exception
|
||||
|
||||
# exception = TimeoutError("Timeout")
|
||||
# status = detector_base.custom_prepare.wait_with_status(
|
||||
# [(my_callback, True)],
|
||||
# check_stopped=True,
|
||||
# timeout=0.01,
|
||||
# interval=0.01,
|
||||
# exception_on_timeout=exception,
|
||||
# )
|
||||
# time.sleep(0.2)
|
||||
# assert status.done is True
|
||||
# assert id(status.exception()) == id(exception)
|
||||
# assert status.success is False
|
||||
|
||||
# detector_base.stopped = False
|
||||
# # Check that standard exception is thrown
|
||||
# status = detector_base.custom_prepare.wait_with_status(
|
||||
# [(my_callback, True)],
|
||||
# check_stopped=True,
|
||||
# timeout=0.01,
|
||||
# interval=0.01,
|
||||
# exception_on_timeout=None,
|
||||
# )
|
||||
# time.sleep(0.2)
|
||||
# assert status.done is True
|
||||
# assert (
|
||||
# status.exception().args
|
||||
# == DeviceTimeoutError(
|
||||
# f"Timeout error for {detector_base.name} while waiting for signals {[(my_callback, True)]}"
|
||||
# ).args
|
||||
# )
|
||||
# assert status.success is False
|
||||
|
@ -16,7 +16,6 @@ from ophyd import Device, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
|
||||
from ophyd_devices.interfaces.protocols.bec_protocols import (
|
||||
BECBaseProtocol,
|
||||
BECDeviceProtocol,
|
||||
BECFlyerProtocol,
|
||||
BECPositionerProtocol,
|
||||
@ -30,6 +29,7 @@ from ophyd_devices.sim.sim_positioner import SimLinearTrajectoryPositioner, SimP
|
||||
from ophyd_devices.sim.sim_signals import ReadOnlySignal
|
||||
from ophyd_devices.sim.sim_utils import H5Writer, LinearTrajectory
|
||||
from ophyd_devices.sim.sim_waveform import SimWaveform
|
||||
from ophyd_devices.tests.utils import get_mock_scan_info
|
||||
from ophyd_devices.utils.bec_device_base import BECDevice, BECDeviceBase
|
||||
|
||||
|
||||
@ -377,7 +377,6 @@ def test_h5proxy(h5proxy_fixture, camera):
|
||||
)
|
||||
camera._registered_proxies.update({h5proxy.name: camera.image.name})
|
||||
camera.sim.params = {"noise": "none", "noise_multiplier": 0}
|
||||
camera.scaninfo.sim_mode = True
|
||||
# pylint: disable=no-member
|
||||
camera.image_shape.set(data.shape[1:])
|
||||
camera.stage()
|
||||
@ -451,15 +450,15 @@ def test_cam_stage_h5writer(camera):
|
||||
mock.patch.object(camera, "h5_writer") as mock_h5_writer,
|
||||
mock.patch.object(camera, "_run_subs") as mock_run_subs,
|
||||
):
|
||||
camera.scaninfo.num_points = 10
|
||||
camera.scaninfo.frames_per_trigger = 1
|
||||
camera.scaninfo.exp_time = 1
|
||||
camera.scan_info.msg.num_points = 10
|
||||
camera.scan_info.msg.scan_parameters["frames_per_trigger"] = 1
|
||||
camera.scan_info.msg.scan_parameters["exp_time"] = 1
|
||||
camera.stage()
|
||||
assert mock_h5_writer.on_stage.call_count == 0
|
||||
camera.unstage()
|
||||
camera.write_to_disk.put(True)
|
||||
camera.stage()
|
||||
calls = [mock.call(file_path="", h5_entry="/entry/data/data")]
|
||||
calls = [mock.call(file_path="./data/test_file_camera.h5", h5_entry="/entry/data/data")]
|
||||
assert mock_h5_writer.on_stage.mock_calls == calls
|
||||
# mock_h5_writer.prepare
|
||||
|
||||
@ -529,17 +528,17 @@ def test_async_monitor_stage(async_monitor):
|
||||
|
||||
def test_async_monitor_prep_random_interval(async_monitor):
|
||||
"""Test the stage method of SimMonitorAsync."""
|
||||
async_monitor.custom_prepare.prep_random_interval()
|
||||
assert async_monitor.custom_prepare._counter == 0
|
||||
async_monitor.prep_random_interval()
|
||||
assert async_monitor._counter == 0
|
||||
assert async_monitor.current_trigger.get() == 0
|
||||
assert 0 < async_monitor.custom_prepare._random_send_interval < 10
|
||||
assert 0 < async_monitor._random_send_interval < 10
|
||||
|
||||
|
||||
def test_async_monitor_complete(async_monitor):
|
||||
"""Test the on_complete method of SimMonitorAsync."""
|
||||
with (
|
||||
mock.patch.object(async_monitor.custom_prepare, "_send_data_to_bec") as mock_send,
|
||||
mock.patch.object(async_monitor.custom_prepare, "prep_random_interval") as mock_prep,
|
||||
mock.patch.object(async_monitor, "_send_data_to_bec") as mock_send,
|
||||
mock.patch.object(async_monitor, "prep_random_interval") as mock_prep,
|
||||
):
|
||||
status = async_monitor.complete()
|
||||
status_wait(status)
|
||||
@ -556,11 +555,11 @@ def test_async_monitor_complete(async_monitor):
|
||||
|
||||
def test_async_mon_on_trigger(async_monitor):
|
||||
"""Test the on_trigger method of SimMonitorAsync."""
|
||||
with (mock.patch.object(async_monitor.custom_prepare, "_send_data_to_bec") as mock_send,):
|
||||
async_monitor.custom_prepare.on_stage()
|
||||
upper_limit = async_monitor.custom_prepare._random_send_interval
|
||||
with (mock.patch.object(async_monitor, "_send_data_to_bec") as mock_send,):
|
||||
async_monitor.on_stage()
|
||||
upper_limit = async_monitor._random_send_interval
|
||||
for ii in range(1, upper_limit + 1):
|
||||
status = async_monitor.custom_prepare.on_trigger()
|
||||
status = async_monitor.on_trigger()
|
||||
status_wait(status)
|
||||
assert async_monitor.current_trigger.get() == ii
|
||||
assert mock_send.call_count == 1
|
||||
@ -568,10 +567,10 @@ def test_async_mon_on_trigger(async_monitor):
|
||||
|
||||
def test_async_mon_send_data_to_bec(async_monitor):
|
||||
"""Test the _send_data_to_bec method of SimMonitorAsync."""
|
||||
async_monitor.scaninfo.scan_msg = SimpleNamespace(metadata={})
|
||||
async_monitor.scan_info = get_mock_scan_info()
|
||||
async_monitor.data_buffer.update({"value": [0, 5], "timestamp": [0, 0]})
|
||||
with mock.patch.object(async_monitor.connector, "xadd") as mock_xadd:
|
||||
async_monitor.custom_prepare._send_data_to_bec()
|
||||
async_monitor._send_data_to_bec()
|
||||
dev_msg = messages.DeviceMessage(
|
||||
signals={async_monitor.readback.name: async_monitor.data_buffer},
|
||||
metadata={"async_update": async_monitor.async_update.get()},
|
||||
@ -580,10 +579,10 @@ def test_async_mon_send_data_to_bec(async_monitor):
|
||||
call = [
|
||||
mock.call(
|
||||
MessageEndpoints.device_async_readback(
|
||||
scan_id=async_monitor.scaninfo.scan_id, device=async_monitor.name
|
||||
scan_id=async_monitor.scan_info.msg.scan_id, device=async_monitor.name
|
||||
),
|
||||
{"data": dev_msg},
|
||||
expire=async_monitor.custom_prepare._stream_ttl,
|
||||
expire=async_monitor._stream_ttl,
|
||||
)
|
||||
]
|
||||
assert mock_xadd.mock_calls == call
|
||||
@ -618,8 +617,8 @@ def test_waveform(waveform):
|
||||
# Now also test the async readback
|
||||
mock_connector = waveform.connector = mock.MagicMock()
|
||||
mock_run_subs = waveform._run_subs = mock.MagicMock()
|
||||
waveform.scaninfo.scan_msg = SimpleNamespace(metadata={})
|
||||
waveform.scaninfo.scan_id = "test"
|
||||
waveform.scan_info = get_mock_scan_info()
|
||||
waveform.scan_info.msg.scan_id = "test"
|
||||
status = waveform.trigger()
|
||||
timer = 0
|
||||
while not status.done:
|
||||
|
Reference in New Issue
Block a user