Merge branch 'eiger_refactor' into 'master'
refactoring and tests for eiger and pilatus detectors Closes #11 and #10 See merge request bec/ophyd_devices!36
This commit is contained in:
commit
47d3a5a65b
@ -25,7 +25,7 @@ from ophyd.quadem import QuadEM
|
|||||||
# cSAXS
|
# cSAXS
|
||||||
from .epics_motor_ex import EpicsMotorEx
|
from .epics_motor_ex import EpicsMotorEx
|
||||||
from .mcs_csaxs import McsCsaxs
|
from .mcs_csaxs import McsCsaxs
|
||||||
from .eiger9m_csaxs import Eiger9mCsaxs
|
from .eiger9m_csaxs import Eiger9McSAXS
|
||||||
from .pilatus_csaxs import PilatusCsaxs
|
from .pilatus_csaxs import PilatuscSAXS
|
||||||
from .falcon_csaxs import FalconCsaxs
|
from .falcon_csaxs import FalconCsaxs
|
||||||
from .DelayGeneratorDG645 import DelayGeneratorDG645
|
from .DelayGeneratorDG645 import DelayGeneratorDG645
|
||||||
|
@ -6,31 +6,88 @@ from bec_lib.core import bec_logger
|
|||||||
logger = bec_logger.logger
|
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 = "mockqueueID111",
|
||||||
|
scan_number: int = 1,
|
||||||
|
exp_time: float = 12e-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:
|
||||||
|
info_msg = {
|
||||||
|
"RID": self.mockrid,
|
||||||
|
"queueID": 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:
|
class BecScaninfoMixin:
|
||||||
def __init__(self, device_manager: DeviceManagerBase = None, sim_mode=False) -> None:
|
"""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, sim_mode: bool = False, bec_info_msg=None
|
||||||
|
) -> None:
|
||||||
self.device_manager = device_manager
|
self.device_manager = device_manager
|
||||||
self.sim_mode = sim_mode
|
self.sim_mode = sim_mode
|
||||||
self.scan_msg = None
|
self.scan_msg = None
|
||||||
self.scanID = None
|
self.scanID = None
|
||||||
self.bec_info_msg = {
|
if bec_info_msg is None:
|
||||||
"RID": "mockrid",
|
infomsgmock = BECInfoMsgMock()
|
||||||
"queueID": "mockqueuid",
|
self.bec_info_msg = infomsgmock.get_bec_info_msg()
|
||||||
"scan_number": 1,
|
else:
|
||||||
"exp_time": 12e-3,
|
self.bec_info_msg = bec_info_msg
|
||||||
"num_points": 500,
|
|
||||||
"readout_time": 3e-3,
|
|
||||||
"scan_type": "fly",
|
|
||||||
"num_lines": 1,
|
|
||||||
"frames_per_trigger": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_bec_info_msg(self) -> None:
|
def get_bec_info_msg(self) -> None:
|
||||||
|
"""Get BECInfoMsg object"""
|
||||||
return self.bec_info_msg
|
return self.bec_info_msg
|
||||||
|
|
||||||
def change_config(self, bec_info_msg: dict) -> None:
|
def change_config(self, bec_info_msg: dict) -> None:
|
||||||
|
"""Change BECInfoMsg object"""
|
||||||
self.bec_info_msg = bec_info_msg
|
self.bec_info_msg = bec_info_msg
|
||||||
|
|
||||||
def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage:
|
def _get_current_scan_msg(self) -> BECMessage.ScanStatusMessage:
|
||||||
|
"""Get current scan message
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BECMessage.ScanStatusMessage: BECMessage.ScanStatusMessage object
|
||||||
|
"""
|
||||||
if not self.sim_mode:
|
if not self.sim_mode:
|
||||||
# TODO what if no scan info is there yet!
|
# TODO what if no scan info is there yet!
|
||||||
msg = self.device_manager.producer.get(MessageEndpoints.scan_status())
|
msg = self.device_manager.producer.get(MessageEndpoints.scan_status())
|
||||||
@ -43,11 +100,16 @@ class BecScaninfoMixin:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_username(self) -> str:
|
def get_username(self) -> str:
|
||||||
|
"""Get username"""
|
||||||
if not self.sim_mode:
|
if not self.sim_mode:
|
||||||
return self.device_manager.producer.get(MessageEndpoints.account()).decode()
|
return self.device_manager.producer.get(MessageEndpoints.account()).decode()
|
||||||
return os.getlogin()
|
return os.getlogin()
|
||||||
|
|
||||||
def load_scan_metadata(self) -> None:
|
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()
|
self.scan_msg = scan_msg = self._get_current_scan_msg()
|
||||||
logger.info(f"{self.scan_msg}")
|
logger.info(f"{self.scan_msg}")
|
||||||
try:
|
try:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import enum
|
import enum
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
|
import threading
|
||||||
from bec_lib.core.devicemanager import DeviceStatus
|
from bec_lib.core.devicemanager import DeviceStatus
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import os
|
import os
|
||||||
@ -16,12 +16,15 @@ from std_daq_client import StdDaqClient
|
|||||||
from bec_lib.core import BECMessage, MessageEndpoints, threadlocked
|
from bec_lib.core import BECMessage, MessageEndpoints, threadlocked
|
||||||
from bec_lib.core.file_utils import FileWriterMixin
|
from bec_lib.core.file_utils import FileWriterMixin
|
||||||
from bec_lib.core import bec_logger
|
from bec_lib.core import bec_logger
|
||||||
|
from bec_lib.core.bec_service import SERVICE_CONFIG
|
||||||
|
|
||||||
from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin
|
from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin
|
||||||
from ophyd_devices.utils import bec_utils
|
from ophyd_devices.utils import bec_utils
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
EIGER9M_MIN_READOUT = 3e-3
|
||||||
|
|
||||||
|
|
||||||
class EigerError(Exception):
|
class EigerError(Exception):
|
||||||
"""Base class for exceptions in this module."""
|
"""Base class for exceptions in this module."""
|
||||||
@ -29,13 +32,20 @@ class EigerError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class EigerTimeoutError(Exception):
|
class EigerTimeoutError(EigerError):
|
||||||
"""Raised when the Eiger does not respond in time during unstage."""
|
"""Raised when the Eiger does not respond in time during unstage."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SlsDetectorCam(Device):
|
class DeviceClassInitError(EigerError):
|
||||||
|
"""Raised when initiation of the device class fails,
|
||||||
|
due to missing device manager or not started in sim_mode."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SLSDetectorCam(Device):
|
||||||
"""SLS Detector Camera - Eiger 9M
|
"""SLS Detector Camera - Eiger 9M
|
||||||
|
|
||||||
Base class to map EPICS PVs to ophyd signals.
|
Base class to map EPICS PVs to ophyd signals.
|
||||||
@ -52,7 +62,7 @@ class SlsDetectorCam(Device):
|
|||||||
detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV")
|
detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV")
|
||||||
|
|
||||||
|
|
||||||
class TriggerSource(int, enum.Enum):
|
class TriggerSource(enum.IntEnum):
|
||||||
"""Trigger signals for Eiger9M detector"""
|
"""Trigger signals for Eiger9M detector"""
|
||||||
|
|
||||||
AUTO = 0
|
AUTO = 0
|
||||||
@ -61,7 +71,7 @@ class TriggerSource(int, enum.Enum):
|
|||||||
BURST_TRIGGER = 3
|
BURST_TRIGGER = 3
|
||||||
|
|
||||||
|
|
||||||
class DetectorState(int, enum.Enum):
|
class DetectorState(enum.IntEnum):
|
||||||
"""Detector states for Eiger9M detector"""
|
"""Detector states for Eiger9M detector"""
|
||||||
|
|
||||||
IDLE = 0
|
IDLE = 0
|
||||||
@ -77,7 +87,7 @@ class DetectorState(int, enum.Enum):
|
|||||||
ABORTED = 10
|
ABORTED = 10
|
||||||
|
|
||||||
|
|
||||||
class Eiger9mCsaxs(DetectorBase):
|
class Eiger9McSAXS(DetectorBase):
|
||||||
"""Eiger 9M detector for CSAXS
|
"""Eiger 9M detector for CSAXS
|
||||||
|
|
||||||
Parent class: DetectorBase
|
Parent class: DetectorBase
|
||||||
@ -94,7 +104,7 @@ class Eiger9mCsaxs(DetectorBase):
|
|||||||
"describe",
|
"describe",
|
||||||
]
|
]
|
||||||
|
|
||||||
cam = ADCpt(SlsDetectorCam, "cam1:")
|
cam = ADCpt(SLSDetectorCam, "cam1:")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -131,34 +141,50 @@ class Eiger9mCsaxs(DetectorBase):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
if device_manager is None and not sim_mode:
|
if device_manager is None and not sim_mode:
|
||||||
raise EigerError("Add DeviceManager to initialization or init with sim_mode=True")
|
raise DeviceClassInitError(
|
||||||
|
f"No device manager for device: {name}, and not started sim_mode: {sim_mode}. Add DeviceManager to initialization or init with sim_mode=True"
|
||||||
# Not sure if this is needed, comment it for now!
|
)
|
||||||
# self._lock = threading.RLock()
|
self.sim_mode = sim_mode
|
||||||
|
# TODO check if threadlock is needed for unstage
|
||||||
|
self._lock = threading.RLock()
|
||||||
self._stopped = False
|
self._stopped = False
|
||||||
self.name = name
|
self.name = name
|
||||||
self.wait_for_connection()
|
self.service_cfg = None
|
||||||
# Spin up connections for simulation or BEC mode
|
self.std_client = None
|
||||||
# TODO check if sim_mode still works. Is it needed? I believe filewriting might be handled properly
|
self.scaninfo = None
|
||||||
|
self.filewriter = None
|
||||||
|
self.readout_time_min = EIGER9M_MIN_READOUT
|
||||||
|
self.std_rest_server_url = (
|
||||||
|
kwargs["file_writer_url"] if "file_writer_url" in kwargs else "http://xbl-daq-29:5000"
|
||||||
|
)
|
||||||
|
self.wait_for_connection(all_signals=True)
|
||||||
if not sim_mode:
|
if not sim_mode:
|
||||||
from bec_lib.core.bec_service import SERVICE_CONFIG
|
self._update_service_config()
|
||||||
|
|
||||||
self.device_manager = device_manager
|
self.device_manager = device_manager
|
||||||
self._producer = self.device_manager.producer
|
|
||||||
self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"]
|
|
||||||
else:
|
else:
|
||||||
base_path = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"
|
self.device_manager = bec_utils.DMMock()
|
||||||
self._producer = bec_utils.MockProducer()
|
base_path = kwargs["basepath"] if "basepath" in kwargs else "~/Data10/"
|
||||||
self.device_manager = bec_utils.MockDeviceManager()
|
self.service_cfg = {"base_path": os.path.expanduser(base_path)}
|
||||||
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
|
self._producer = self.device_manager.producer
|
||||||
self.scaninfo.load_scan_metadata()
|
self._update_scaninfo()
|
||||||
self.service_cfg = {"base_path": base_path}
|
self._update_filewriter()
|
||||||
|
|
||||||
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
|
|
||||||
self.scaninfo.load_scan_metadata()
|
|
||||||
self.filewriter = FileWriterMixin(self.service_cfg)
|
|
||||||
self._init()
|
self._init()
|
||||||
|
|
||||||
|
def _update_filewriter(self) -> None:
|
||||||
|
"""Update filewriter with service config"""
|
||||||
|
self.filewriter = FileWriterMixin(self.service_cfg)
|
||||||
|
|
||||||
|
def _update_scaninfo(self) -> None:
|
||||||
|
"""Update scaninfo from BecScaninfoMixing
|
||||||
|
This depends on device manager and operation/sim_mode
|
||||||
|
"""
|
||||||
|
self.scaninfo = BecScaninfoMixin(self.device_manager, self.sim_mode)
|
||||||
|
self.scaninfo.load_scan_metadata()
|
||||||
|
|
||||||
|
def _update_service_config(self) -> None:
|
||||||
|
"""Update service config from BEC service config"""
|
||||||
|
self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"]
|
||||||
|
|
||||||
# TODO function for abstract class?
|
# TODO function for abstract class?
|
||||||
def _init(self) -> None:
|
def _init(self) -> None:
|
||||||
"""Initialize detector, filewriter and set default parameters"""
|
"""Initialize detector, filewriter and set default parameters"""
|
||||||
@ -166,12 +192,19 @@ class Eiger9mCsaxs(DetectorBase):
|
|||||||
self._init_detector()
|
self._init_detector()
|
||||||
self._init_filewriter()
|
self._init_filewriter()
|
||||||
|
|
||||||
# TODO function for abstract class?
|
|
||||||
def _default_parameter(self) -> None:
|
def _default_parameter(self) -> None:
|
||||||
"""Set default parameters for Eiger 9M
|
"""Set default parameters for Pilatus300k detector
|
||||||
readout (float): readout time in seconds
|
readout (float): readout time in seconds
|
||||||
"""
|
"""
|
||||||
self.reduce_readout = 1e-3
|
self._update_readout_time()
|
||||||
|
|
||||||
|
def _update_readout_time(self) -> None:
|
||||||
|
readout_time = (
|
||||||
|
self.scaninfo.readout_time
|
||||||
|
if hasattr(self.scaninfo, "readout_time")
|
||||||
|
else self.readout_time_min
|
||||||
|
)
|
||||||
|
self.readout_time = max(readout_time, self.readout_time_min)
|
||||||
|
|
||||||
# TODO function for abstract class?
|
# TODO function for abstract class?
|
||||||
def _init_detector(self) -> None:
|
def _init_detector(self) -> None:
|
||||||
@ -188,20 +221,19 @@ class Eiger9mCsaxs(DetectorBase):
|
|||||||
For the Eiger9M, the data backend is std_daq client.
|
For the Eiger9M, the data backend is std_daq client.
|
||||||
Setting up these parameters depends on the backend, and would need to change upon changes in the backend.
|
Setting up these parameters depends on the backend, and would need to change upon changes in the backend.
|
||||||
"""
|
"""
|
||||||
self.std_rest_server_url = "http://xbl-daq-29:5000"
|
|
||||||
self.std_client = StdDaqClient(url_base=self.std_rest_server_url)
|
self.std_client = StdDaqClient(url_base=self.std_rest_server_url)
|
||||||
self.std_client.stop_writer()
|
self.std_client.stop_writer()
|
||||||
timeout = 0
|
timeout = 0
|
||||||
# TODO changing e-account was not possible during beamtimes.
|
# TODO put back change of e-account! and check with Leo which status to wait for
|
||||||
# self._update_std_cfg("writer_user_id", int(self.scaninfo.username.strip(" e")))
|
eacc = self.scaninfo.username
|
||||||
# time.sleep(5)
|
self._update_std_cfg("writer_user_id", int(eacc.strip(" e")))
|
||||||
# TODO is this the only state to wait for or should we wait for more from the std_daq client?
|
time.sleep(5)
|
||||||
while not self.std_client.get_status()["state"] == "READY":
|
while not self.std_client.get_status()["state"] == "READY":
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
timeout = timeout + 0.1
|
timeout = timeout + 0.1
|
||||||
logger.info("Waiting for std_daq init.")
|
logger.info("Waiting for std_daq init.")
|
||||||
if timeout > 5:
|
if timeout > 5:
|
||||||
if not self.std_client.get_status()["state"]:
|
if not self.std_client.get_status()["state"] == "READY":
|
||||||
raise EigerError(
|
raise EigerError(
|
||||||
f"Std client not in READY state, returns: {self.std_client.get_status()}"
|
f"Std client not in READY state, returns: {self.std_client.get_status()}"
|
||||||
)
|
)
|
||||||
@ -252,26 +284,32 @@ class Eiger9mCsaxs(DetectorBase):
|
|||||||
self._prep_file_writer()
|
self._prep_file_writer()
|
||||||
self._prep_det()
|
self._prep_det()
|
||||||
state = False
|
state = False
|
||||||
self._publish_file_location(done=state, successful=state)
|
self._publish_file_location(done=state)
|
||||||
self._arm_acquisition()
|
self._arm_acquisition()
|
||||||
# TODO Fix should take place in EPICS or directly on the hardware!
|
# TODO Fix should take place in EPICS or directly on the hardware!
|
||||||
# We observed that the detector missed triggers in the beginning in case BEC was to fast. Adding 50ms delay solved this
|
# We observed that the detector missed triggers in the beginning in case BEC was to fast. Adding 50ms delay solved this
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
return super().stage()
|
return super().stage()
|
||||||
|
|
||||||
|
def _filepath_exists(self, filepath: str) -> None:
|
||||||
|
timer = 0
|
||||||
|
while not os.path.exists(os.path.dirname(self.filepath)):
|
||||||
|
timer = time + 0.1
|
||||||
|
time.sleep(0.1)
|
||||||
|
if timer > 3:
|
||||||
|
raise EigerError(f"Timeout of 3s reached for filepath {self.filepath}")
|
||||||
|
|
||||||
# TODO function for abstract class?
|
# TODO function for abstract class?
|
||||||
def _prep_file_writer(self) -> None:
|
def _prep_file_writer(self) -> None:
|
||||||
"""Prepare file writer for scan
|
"""Prepare file writer for scan
|
||||||
|
|
||||||
self.filewriter is a FileWriterMixin object that hosts logic for compiling the filepath
|
self.filewriter is a FileWriterMixin object that hosts logic for compiling the filepath
|
||||||
"""
|
"""
|
||||||
|
timer = 0
|
||||||
self.filepath = self.filewriter.compile_full_filename(
|
self.filepath = self.filewriter.compile_full_filename(
|
||||||
self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True
|
self.scaninfo.scan_number, f"{self.name}.h5", 1000, 5, True
|
||||||
)
|
)
|
||||||
# TODO needed, should be checked from the filerwriter mixin right?
|
self._filepath_exists(self.filepath)
|
||||||
while not os.path.exists(os.path.dirname(self.filepath)):
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
self._stop_file_writer()
|
self._stop_file_writer()
|
||||||
logger.info(f" std_daq output filepath {self.filepath}")
|
logger.info(f" std_daq output filepath {self.filepath}")
|
||||||
# TODO Discuss with Leo if this is needed, or how to start the async writing best
|
# TODO Discuss with Leo if this is needed, or how to start the async writing best
|
||||||
@ -288,16 +326,22 @@ class Eiger9mCsaxs(DetectorBase):
|
|||||||
raise EigerError(f"Timeout of start_writer_async with {exc}")
|
raise EigerError(f"Timeout of start_writer_async with {exc}")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
timer = timer + 0.01
|
||||||
det_ctrl = self.std_client.get_status()["acquisition"]["state"]
|
det_ctrl = self.std_client.get_status()["acquisition"]["state"]
|
||||||
if det_ctrl == "WAITING_IMAGES":
|
if det_ctrl == "WAITING_IMAGES":
|
||||||
break
|
break
|
||||||
time.sleep(0.005)
|
time.sleep(0.01)
|
||||||
|
if timer > 5:
|
||||||
|
self._close_file_writer()
|
||||||
|
raise EigerError(
|
||||||
|
f"Timeout of 5s reached for std_daq start_writer_async with std_daq client status {det_ctrl}"
|
||||||
|
)
|
||||||
|
|
||||||
# TODO function for abstract class?
|
# TODO function for abstract class?
|
||||||
def _stop_file_writer(self) -> None:
|
def _stop_file_writer(self) -> None:
|
||||||
"""Close file writer"""
|
"""Close file writer"""
|
||||||
self.std_client.stop_writer()
|
self.std_client.stop_writer()
|
||||||
# TODO can I wait for a status message here maybe? To ensure writer returned
|
# TODO can I wait for a status message here maybe? To ensure writer stopped and returned
|
||||||
|
|
||||||
# TODO function for abstract class?
|
# TODO function for abstract class?
|
||||||
def _prep_det(self) -> None:
|
def _prep_det(self) -> None:
|
||||||
@ -312,7 +356,8 @@ class Eiger9mCsaxs(DetectorBase):
|
|||||||
"""Set correct detector threshold to 1/2 of current X-ray energy, allow 5% tolerance"""
|
"""Set correct detector threshold to 1/2 of current X-ray energy, allow 5% tolerance"""
|
||||||
# threshold energy might be in eV or keV
|
# threshold energy might be in eV or keV
|
||||||
factor = 1
|
factor = 1
|
||||||
if self.cam.threshold_energy._metadata["units"] == "eV":
|
unit = getattr(self.cam.threshold_energy, "units", None)
|
||||||
|
if unit != None and unit == "eV":
|
||||||
factor = 1000
|
factor = 1000
|
||||||
setpoint = int(self.mokev * factor)
|
setpoint = int(self.mokev * factor)
|
||||||
energy = self.cam.beam_energy.read()[self.cam.beam_energy.name]["value"]
|
energy = self.cam.beam_energy.read()[self.cam.beam_energy.name]["value"]
|
||||||
@ -326,40 +371,60 @@ class Eiger9mCsaxs(DetectorBase):
|
|||||||
"""Set acquisition parameters for the detector"""
|
"""Set acquisition parameters for the detector"""
|
||||||
self.cam.num_images.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger))
|
self.cam.num_images.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger))
|
||||||
self.cam.num_frames.put(1)
|
self.cam.num_frames.put(1)
|
||||||
|
self._update_readout_time()
|
||||||
|
|
||||||
# TODO function for abstract class? + call it for each scan??
|
# TODO function for abstract class? + call it for each scan??
|
||||||
def _set_trigger(self, trigger_source: TriggerSource) -> None:
|
def _set_trigger(self, trigger_source: TriggerSource) -> None:
|
||||||
"""Set trigger source for the detector.
|
"""Set trigger source for the detector.
|
||||||
Check the TriggerSource enum for possible values
|
Check the TriggerSource enum for possible values
|
||||||
|
|
||||||
|
Args:
|
||||||
|
trigger_source (TriggerSource): Trigger source for the detector
|
||||||
|
|
||||||
"""
|
"""
|
||||||
value = int(trigger_source)
|
value = trigger_source
|
||||||
self.cam.trigger_mode.put(value)
|
self.cam.trigger_mode.put(value)
|
||||||
|
|
||||||
def _publish_file_location(self, done=False, successful=False) -> None:
|
def _publish_file_location(self, done: bool = False, successful: bool = None) -> None:
|
||||||
"""Publish the filepath to REDIS
|
"""Publish the filepath to REDIS.
|
||||||
First msg for file writer and the second one for other listeners (e.g. radial integ)
|
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
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pipe = self._producer.pipeline()
|
pipe = self._producer.pipeline()
|
||||||
|
if successful is None:
|
||||||
|
msg = BECMessage.FileMessage(file_path=self.filepath, done=done)
|
||||||
|
else:
|
||||||
msg = BECMessage.FileMessage(file_path=self.filepath, done=done, successful=successful)
|
msg = BECMessage.FileMessage(file_path=self.filepath, done=done, successful=successful)
|
||||||
self._producer.set_and_publish(
|
self._producer.set_and_publish(
|
||||||
MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), pipe=pipe
|
MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), pipe=pipe
|
||||||
)
|
)
|
||||||
self._producer.set_and_publish(
|
self._producer.set_and_publish(
|
||||||
MessageEndpoints.file_event(self.name), msg.dumps(), pip=pipe
|
MessageEndpoints.file_event(self.name), msg.dumps(), pipe=pipe
|
||||||
)
|
)
|
||||||
pipe.execute()
|
pipe.execute()
|
||||||
|
|
||||||
# TODO function for abstract class?
|
# TODO function for abstract class?
|
||||||
def _arm_acquisition(self) -> None:
|
def _arm_acquisition(self) -> None:
|
||||||
"""Arm Eiger detector for acquisition"""
|
"""Arm Eiger detector for acquisition"""
|
||||||
|
timer = 0
|
||||||
self.cam.acquire.put(1)
|
self.cam.acquire.put(1)
|
||||||
while True:
|
while True:
|
||||||
det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"]
|
det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"]
|
||||||
if det_ctrl == int(DetectorState.RUNNING):
|
if det_ctrl == DetectorState.RUNNING:
|
||||||
break
|
break
|
||||||
if self._stopped == True:
|
if self._stopped == True:
|
||||||
break
|
break
|
||||||
time.sleep(0.005)
|
time.sleep(0.01)
|
||||||
|
timer += 0.01
|
||||||
|
if timer > 5:
|
||||||
|
self.stop()
|
||||||
|
raise EigerTimeoutError("Failed to arm the acquisition. IOC did not update.")
|
||||||
|
|
||||||
# TODO function for abstract class?
|
# TODO function for abstract class?
|
||||||
def trigger(self) -> DeviceStatus:
|
def trigger(self) -> DeviceStatus:
|
||||||
@ -419,15 +484,13 @@ class Eiger9mCsaxs(DetectorBase):
|
|||||||
# Check status with timeout, break out if _stopped=True
|
# Check status with timeout, break out if _stopped=True
|
||||||
while True:
|
while True:
|
||||||
det_ctrl = self.cam.acquire.read()[self.cam.acquire.name]["value"]
|
det_ctrl = self.cam.acquire.read()[self.cam.acquire.name]["value"]
|
||||||
std_ctrl = self.std_client.get_status()["acquisition"]["state"]
|
|
||||||
status = self.std_client.get_status()
|
status = self.std_client.get_status()
|
||||||
|
std_ctrl = status["acquisition"]["state"]
|
||||||
received_frames = status["acquisition"]["stats"]["n_write_completed"]
|
received_frames = status["acquisition"]["stats"]["n_write_completed"]
|
||||||
total_frames = int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)
|
total_frames = int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)
|
||||||
if det_ctrl == 0 and std_ctrl == "FINISHED" and total_frames == received_frames:
|
if det_ctrl == 0 and std_ctrl == "FINISHED" and total_frames == received_frames:
|
||||||
break
|
break
|
||||||
if self._stopped == True:
|
if self._stopped == True:
|
||||||
self._stop_det()
|
|
||||||
self._stop_file_writer()
|
|
||||||
break
|
break
|
||||||
time.sleep(sleep_time)
|
time.sleep(sleep_time)
|
||||||
timer += sleep_time
|
timer += sleep_time
|
||||||
@ -452,7 +515,7 @@ class Eiger9mCsaxs(DetectorBase):
|
|||||||
# Check status
|
# Check status
|
||||||
while True:
|
while True:
|
||||||
det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"]
|
det_ctrl = self.cam.detector_state.read()[self.cam.detector_state.name]["value"]
|
||||||
if det_ctrl == int(DetectorState.IDLE):
|
if det_ctrl == DetectorState.IDLE:
|
||||||
break
|
break
|
||||||
if self._stopped == True:
|
if self._stopped == True:
|
||||||
break
|
break
|
||||||
@ -474,4 +537,4 @@ class Eiger9mCsaxs(DetectorBase):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
eiger = Eiger9mCsaxs(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True)
|
eiger = Eiger9McSAXS(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True)
|
||||||
|
@ -14,6 +14,7 @@ from ophyd import ADComponent as ADCpt
|
|||||||
|
|
||||||
from bec_lib.core import BECMessage, MessageEndpoints
|
from bec_lib.core import BECMessage, MessageEndpoints
|
||||||
from bec_lib.core.file_utils import FileWriterMixin
|
from bec_lib.core.file_utils import FileWriterMixin
|
||||||
|
from bec_lib.core.bec_service import SERVICE_CONFIG
|
||||||
from bec_lib.core import bec_logger
|
from bec_lib.core import bec_logger
|
||||||
|
|
||||||
from ophyd_devices.utils import bec_utils as bec_utils
|
from ophyd_devices.utils import bec_utils as bec_utils
|
||||||
@ -21,6 +22,8 @@ from ophyd_devices.epics.devices.bec_scaninfo_mixin import BecScaninfoMixin
|
|||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
PILATUS_MIN_READOUT = 3e-3
|
||||||
|
|
||||||
|
|
||||||
class PilatusError(Exception):
|
class PilatusError(Exception):
|
||||||
"""Base class for exceptions in this module."""
|
"""Base class for exceptions in this module."""
|
||||||
@ -28,13 +31,20 @@ class PilatusError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PilatusTimeoutError(Exception):
|
class PilatusTimeoutError(PilatusError):
|
||||||
"""Raised when the Pilatus does not respond in time during unstage."""
|
"""Raised when the Pilatus does not respond in time during unstage."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TriggerSource(int, enum.Enum):
|
class DeviceClassInitError(PilatusError):
|
||||||
|
"""Raised when initiation of the device class fails,
|
||||||
|
due to missing device manager or not started in sim_mode."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TriggerSource(enum.IntEnum):
|
||||||
INTERNAL = 0
|
INTERNAL = 0
|
||||||
EXT_ENABLE = 1
|
EXT_ENABLE = 1
|
||||||
EXT_TRIGGER = 2
|
EXT_TRIGGER = 2
|
||||||
@ -42,14 +52,14 @@ class TriggerSource(int, enum.Enum):
|
|||||||
ALGINMENT = 4
|
ALGINMENT = 4
|
||||||
|
|
||||||
|
|
||||||
class SlsDetectorCam(Device):
|
class SLSDetectorCam(Device):
|
||||||
"""SLS Detector Camera - Pilatus
|
"""SLS Detector Camera - Pilatus
|
||||||
|
|
||||||
Base class to map EPICS PVs to ophyd signals.
|
Base class to map EPICS PVs to ophyd signals.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
num_images = ADCpt(EpicsSignalWithRBV, "NumImages")
|
num_images = ADCpt(EpicsSignalWithRBV, "NumImages")
|
||||||
num_exposures = ADCpt(EpicsSignalWithRBV, "NumExposures")
|
num_frames = ADCpt(EpicsSignalWithRBV, "NumExposures")
|
||||||
delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures")
|
delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures")
|
||||||
trigger_mode = ADCpt(EpicsSignalWithRBV, "TriggerMode")
|
trigger_mode = ADCpt(EpicsSignalWithRBV, "TriggerMode")
|
||||||
acquire = ADCpt(EpicsSignal, "Acquire")
|
acquire = ADCpt(EpicsSignal, "Acquire")
|
||||||
@ -70,7 +80,7 @@ class SlsDetectorCam(Device):
|
|||||||
gap_fill = ADCpt(EpicsSignalWithRBV, "GapFill")
|
gap_fill = ADCpt(EpicsSignalWithRBV, "GapFill")
|
||||||
|
|
||||||
|
|
||||||
class PilatusCsaxs(DetectorBase):
|
class PilatuscSAXS(DetectorBase):
|
||||||
"""Pilatus_2 300k detector for CSAXS
|
"""Pilatus_2 300k detector for CSAXS
|
||||||
|
|
||||||
Parent class: DetectorBase
|
Parent class: DetectorBase
|
||||||
@ -87,7 +97,7 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
"describe",
|
"describe",
|
||||||
]
|
]
|
||||||
|
|
||||||
cam = ADCpt(SlsDetectorCam, "cam1:")
|
cam = ADCpt(SLSDetectorCam, "cam1:")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -124,30 +134,46 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
if device_manager is None and not sim_mode:
|
if device_manager is None and not sim_mode:
|
||||||
raise PilatusError("Add DeviceManager to initialization or init with sim_mode=True")
|
raise DeviceClassInitError(
|
||||||
|
f"No device manager for device: {name}, and not started sim_mode: {sim_mode}. Add DeviceManager to initialization or init with sim_mode=True"
|
||||||
|
)
|
||||||
|
self.sim_mode = sim_mode
|
||||||
|
self._stopped = False
|
||||||
self.name = name
|
self.name = name
|
||||||
self.wait_for_connection()
|
self.service_cfg = None
|
||||||
# Spin up connections for simulation or BEC mode
|
self.std_client = None
|
||||||
|
self.scaninfo = None
|
||||||
|
self.filewriter = None
|
||||||
|
self.readout_time_min = PILATUS_MIN_READOUT
|
||||||
|
# TODO move url from data backend up here?
|
||||||
|
self.wait_for_connection(all_signals=True)
|
||||||
if not sim_mode:
|
if not sim_mode:
|
||||||
from bec_lib.core.bec_service import SERVICE_CONFIG
|
self._update_service_config()
|
||||||
|
|
||||||
self.device_manager = device_manager
|
self.device_manager = device_manager
|
||||||
self._producer = self.device_manager.producer
|
|
||||||
self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"]
|
|
||||||
else:
|
else:
|
||||||
base_path = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/"
|
self.device_manager = bec_utils.DMMock()
|
||||||
self._producer = bec_utils.MockProducer()
|
base_path = kwargs["basepath"] if "basepath" in kwargs else "~/Data10/"
|
||||||
self.device_manager = bec_utils.MockDeviceManager()
|
self.service_cfg = {"base_path": os.path.expanduser(base_path)}
|
||||||
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
|
self._producer = self.device_manager.producer
|
||||||
self.scaninfo.load_scan_metadata()
|
self._update_scaninfo()
|
||||||
self.service_cfg = {"base_path": base_path}
|
self._update_filewriter()
|
||||||
|
|
||||||
self.scaninfo = BecScaninfoMixin(device_manager, sim_mode)
|
|
||||||
self.scaninfo.load_scan_metadata()
|
|
||||||
self.filewriter = FileWriterMixin(self.service_cfg)
|
|
||||||
self._init()
|
self._init()
|
||||||
|
|
||||||
|
def _update_filewriter(self) -> None:
|
||||||
|
"""Update filewriter with service config"""
|
||||||
|
self.filewriter = FileWriterMixin(self.service_cfg)
|
||||||
|
|
||||||
|
def _update_scaninfo(self) -> None:
|
||||||
|
"""Update scaninfo from BecScaninfoMixing
|
||||||
|
This depends on device manager and operation/sim_mode
|
||||||
|
"""
|
||||||
|
self.scaninfo = BecScaninfoMixin(self.device_manager, self.sim_mode)
|
||||||
|
self.scaninfo.load_scan_metadata()
|
||||||
|
|
||||||
|
def _update_service_config(self) -> None:
|
||||||
|
"""Update service config from BEC service config"""
|
||||||
|
self.service_cfg = SERVICE_CONFIG.config["service_config"]["file_writer"]
|
||||||
|
|
||||||
def _init(self) -> None:
|
def _init(self) -> None:
|
||||||
"""Initialize detector, filewriter and set default parameters"""
|
"""Initialize detector, filewriter and set default parameters"""
|
||||||
self._default_parameter()
|
self._default_parameter()
|
||||||
@ -158,12 +184,21 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
"""Set default parameters for Pilatus300k detector
|
"""Set default parameters for Pilatus300k detector
|
||||||
readout (float): readout time in seconds
|
readout (float): readout time in seconds
|
||||||
"""
|
"""
|
||||||
self.reduce_readout = 1e-3
|
self._update_readout_time()
|
||||||
|
|
||||||
|
def _update_readout_time(self) -> None:
|
||||||
|
readout_time = (
|
||||||
|
self.scaninfo.readout_time
|
||||||
|
if hasattr(self.scaninfo, "readout_time")
|
||||||
|
else self.readout_time_min
|
||||||
|
)
|
||||||
|
self.readout_time = max(readout_time, self.readout_time_min)
|
||||||
|
|
||||||
def _init_detector(self) -> None:
|
def _init_detector(self) -> None:
|
||||||
"""Initialize the detector"""
|
"""Initialize the detector"""
|
||||||
# TODO add check if detector is running
|
# TODO add check if detector is running
|
||||||
pass
|
self._stop_det()
|
||||||
|
self._set_trigger(TriggerSource.EXT_ENABLE)
|
||||||
|
|
||||||
def _init_filewriter(self) -> None:
|
def _init_filewriter(self) -> None:
|
||||||
"""Initialize the file writer"""
|
"""Initialize the file writer"""
|
||||||
@ -174,26 +209,28 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
# TODO slow reaction, seemed to have timeout.
|
# TODO slow reaction, seemed to have timeout.
|
||||||
self._set_det_threshold()
|
self._set_det_threshold()
|
||||||
self._set_acquisition_params()
|
self._set_acquisition_params()
|
||||||
|
self._set_trigger(TriggerSource.EXT_ENABLE)
|
||||||
|
|
||||||
def _set_det_threshold(self) -> None:
|
def _set_det_threshold(self) -> None:
|
||||||
# threshold_energy PV exists on Eiger 9M?
|
# threshold_energy PV exists on Eiger 9M?
|
||||||
factor = 1
|
factor = 1
|
||||||
if self.cam.threshold_energy._metadata["units"] == "eV":
|
unit = getattr(self.cam.threshold_energy, "units", None)
|
||||||
|
if unit != None and unit == "eV":
|
||||||
factor = 1000
|
factor = 1000
|
||||||
setpoint = int(self.mokev * factor)
|
setpoint = int(self.mokev * factor)
|
||||||
threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"]
|
threshold = self.cam.threshold_energy.read()[self.cam.threshold_energy.name]["value"]
|
||||||
if not np.isclose(setpoint / 2, threshold, rtol=0.05):
|
if not np.isclose(setpoint / 2, threshold, rtol=0.05):
|
||||||
self.cam.threshold_energy.set(setpoint / 2)
|
self.cam.threshold_energy.put(setpoint / 2)
|
||||||
|
|
||||||
def _set_acquisition_params(self) -> None:
|
def _set_acquisition_params(self) -> None:
|
||||||
"""set acquisition parameters on the detector"""
|
"""set acquisition parameters on the detector"""
|
||||||
# self.cam.acquire_time.set(self.exp_time)
|
# self.cam.acquire_time.set(self.exp_time)
|
||||||
# self.cam.acquire_period.set(self.exp_time + self.readout)
|
# self.cam.acquire_period.set(self.exp_time + self.readout)
|
||||||
self.cam.num_images.set(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger))
|
self.cam.num_images.put(int(self.scaninfo.num_points * self.scaninfo.frames_per_trigger))
|
||||||
self.cam.num_exposures.set(1)
|
self.cam.num_frames.put(1)
|
||||||
self._set_trigger(TriggerSource.EXT_ENABLE) # EXT_TRIGGER)
|
self._update_readout_time()
|
||||||
|
|
||||||
def _set_trigger(self, trigger_source: TriggerSource) -> None:
|
def _set_trigger(self, trigger_source: int) -> None:
|
||||||
"""Set trigger source for the detector, either directly to value or TriggerSource.* with
|
"""Set trigger source for the detector, either directly to value or TriggerSource.* with
|
||||||
INTERNAL = 0
|
INTERNAL = 0
|
||||||
EXT_ENABLE = 1
|
EXT_ENABLE = 1
|
||||||
@ -201,19 +238,22 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
MULTI_TRIGGER = 3
|
MULTI_TRIGGER = 3
|
||||||
ALGINMENT = 4
|
ALGINMENT = 4
|
||||||
"""
|
"""
|
||||||
value = int(trigger_source)
|
value = trigger_source
|
||||||
self.cam.trigger_mode.set(value)
|
self.cam.trigger_mode.put(value)
|
||||||
|
|
||||||
|
def _create_directory(filepath: str) -> None:
|
||||||
|
"""Create directory if it does not exist"""
|
||||||
|
os.makedirs(filepath, exist_ok=True)
|
||||||
|
|
||||||
def _prep_file_writer(self) -> None:
|
def _prep_file_writer(self) -> None:
|
||||||
"""Prepare the file writer for pilatus_2
|
|
||||||
a zmq service is running on xbl-daq-34 that is waiting
|
|
||||||
for a zmq message to start the writer for the pilatus_2 x12sa-pd-2
|
|
||||||
"""
|
"""
|
||||||
# TODO worked reliable with time.sleep(2)
|
Prepare the file writer for pilatus_2
|
||||||
# self._close_file_writer()
|
|
||||||
# time.sleep(2)
|
A zmq service is running on xbl-daq-34 that is waiting
|
||||||
# self._stop_file_writer()
|
for a zmq message to start the writer for the pilatus_2 x12sa-pd-2
|
||||||
# time.sleep(2)
|
|
||||||
|
"""
|
||||||
|
# TODO explore required sleep time here
|
||||||
self._close_file_writer()
|
self._close_file_writer()
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
self._stop_file_writer()
|
self._stop_file_writer()
|
||||||
@ -229,6 +269,7 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
self.cam.file_format.put(0) # 0: TIFF
|
self.cam.file_format.put(0) # 0: TIFF
|
||||||
self.cam.file_template.put("%s%s_%5.5d.cbf")
|
self.cam.file_template.put("%s%s_%5.5d.cbf")
|
||||||
|
|
||||||
|
# TODO remove hardcoded filepath here
|
||||||
# compile filename
|
# compile filename
|
||||||
basepath = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/pilatus_2/"
|
basepath = f"/sls/X12SA/data/{self.scaninfo.username}/Data10/pilatus_2/"
|
||||||
self.filepath = os.path.join(
|
self.filepath = os.path.join(
|
||||||
@ -236,8 +277,11 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
self.filewriter.get_scan_directory(self.scaninfo.scan_number, 1000, 5),
|
self.filewriter.get_scan_directory(self.scaninfo.scan_number, 1000, 5),
|
||||||
)
|
)
|
||||||
# Make directory if needed
|
# Make directory if needed
|
||||||
os.makedirs(self.filepath, exist_ok=True)
|
self._create_directory(self.filepath)
|
||||||
|
|
||||||
|
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||||
|
# start the stream on x12sa-pd-2
|
||||||
|
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
|
||||||
data_msg = {
|
data_msg = {
|
||||||
"source": [
|
"source": [
|
||||||
{
|
{
|
||||||
@ -247,21 +291,14 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
res = self._send_requests_put(url=url, data=data_msg, headers=headers)
|
||||||
logger.info(data_msg)
|
|
||||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
|
||||||
|
|
||||||
res = requests.put(
|
|
||||||
url="http://x12sa-pd-2:8080/stream/pilatus_2",
|
|
||||||
data=json.dumps(data_msg),
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
logger.info(f"{res.status_code} - {res.text} - {res.content}")
|
logger.info(f"{res.status_code} - {res.text} - {res.content}")
|
||||||
|
|
||||||
if not res.ok:
|
if not res.ok:
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
|
|
||||||
# prepare writer
|
# start the data receiver on xbl-daq-34
|
||||||
|
url = "http://xbl-daq-34:8091/pilatus_2/run"
|
||||||
data_msg = [
|
data_msg = [
|
||||||
"zmqWriter",
|
"zmqWriter",
|
||||||
self.scaninfo.username,
|
self.scaninfo.username,
|
||||||
@ -274,14 +311,7 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
"user": self.scaninfo.username,
|
"user": self.scaninfo.username,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
res = self._send_requests_put(url=url, data=data_msg, headers=headers)
|
||||||
res = requests.put(
|
|
||||||
url="http://xbl-daq-34:8091/pilatus_2/run",
|
|
||||||
data=json.dumps(data_msg),
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
# subprocess.run("curl -i -s -X PUT http://xbl-daq-34:8091/pilatus_2/run -d '[\"zmqWriter\",\"e20636\",{\"addr\":\"tcp://x12sa-pd-2:8888\",\"dst\":[\"file\"],\"numFrm\":10,\"timeout\":2000,\"ifType\":\"PULL\",\"user\":\"e20636\"}]'", shell=True)
|
|
||||||
|
|
||||||
logger.info(f"{res.status_code} - {res.text} - {res.content}")
|
logger.info(f"{res.status_code} - {res.text} - {res.content}")
|
||||||
|
|
||||||
if not res.ok:
|
if not res.ok:
|
||||||
@ -289,8 +319,10 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
|
|
||||||
# Wait for server to become available again
|
# Wait for server to become available again
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
logger.info(f"{res.status_code} -{res.text} - {res.content}")
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
# Sent requests.put to xbl-daq-34 to wait for data
|
||||||
|
url = "http://xbl-daq-34:8091/pilatus_2/wait"
|
||||||
data_msg = [
|
data_msg = [
|
||||||
"zmqWriter",
|
"zmqWriter",
|
||||||
self.scaninfo.username,
|
self.scaninfo.username,
|
||||||
@ -299,15 +331,8 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
"timeout": 2000,
|
"timeout": 2000,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
logger.info(f"{res.status_code} -{res.text} - {res.content}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = requests.put(
|
res = self._send_requests_put(url=url, data=data_msg, headers=headers)
|
||||||
url="http://xbl-daq-34:8091/pilatus_2/wait",
|
|
||||||
data=json.dumps(data_msg),
|
|
||||||
# headers=headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"{res}")
|
logger.info(f"{res}")
|
||||||
|
|
||||||
if not res.ok:
|
if not res.ok:
|
||||||
@ -315,25 +340,56 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.info(f"Pilatus2 wait threw Exception: {exc}")
|
logger.info(f"Pilatus2 wait threw Exception: {exc}")
|
||||||
|
|
||||||
def _close_file_writer(self) -> None:
|
def _send_requests_put(self, url: str, data_msg: list = None, headers: dict = None) -> object:
|
||||||
"""Close the file writer for pilatus_2
|
|
||||||
a zmq service is running on xbl-daq-34 that is waiting
|
|
||||||
for a zmq message to stop the writer for the pilatus_2 x12sa-pd-2
|
|
||||||
"""
|
"""
|
||||||
|
Send a put request to the given url
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str): url to send the request to
|
||||||
|
data (dict): data to be sent with the request (optional)
|
||||||
|
headers (dict): headers to be sent with the request (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
status code of the request
|
||||||
|
"""
|
||||||
|
return requests.put(url=url, data=json.dumps(data_msg), headers=headers)
|
||||||
|
|
||||||
|
def _send_requests_delete(self, url: str, headers: dict = None) -> object:
|
||||||
|
"""
|
||||||
|
Send a delete request to the given url
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str): url to send the request to
|
||||||
|
headers (dict): headers to be sent with the request (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
status code of the request
|
||||||
|
"""
|
||||||
|
return requests.delete(url=url, headers=headers)
|
||||||
|
|
||||||
|
def _close_file_writer(self) -> None:
|
||||||
|
"""
|
||||||
|
Close the file writer for pilatus_2
|
||||||
|
|
||||||
|
Delete the data from x12sa-pd-2
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
|
||||||
try:
|
try:
|
||||||
res = requests.delete(url="http://x12sa-pd-2:8080/stream/pilatus_2")
|
res = self._send_requests_delete(url=url)
|
||||||
if not res.ok:
|
if not res.ok:
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.info(f"Pilatus2 delete threw Exception: {exc}")
|
logger.info(f"Pilatus2 close threw Exception: {exc}")
|
||||||
|
|
||||||
def _stop_file_writer(self) -> None:
|
def _stop_file_writer(self) -> None:
|
||||||
res = requests.put(
|
"""
|
||||||
url="http://xbl-daq-34:8091/pilatus_2/stop",
|
Stop the file writer for pilatus_2
|
||||||
# data=json.dumps(data_msg),
|
|
||||||
# headers=headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
Runs on xbl-daq-34
|
||||||
|
"""
|
||||||
|
url = "http://xbl-daq-34:8091/pilatus_2/stop"
|
||||||
|
res = self._send_requests_put(url=url)
|
||||||
if not res.ok:
|
if not res.ok:
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
|
|
||||||
@ -362,28 +418,44 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
self._prep_file_writer()
|
self._prep_file_writer()
|
||||||
self._prep_det()
|
self._prep_det()
|
||||||
state = False
|
state = False
|
||||||
self._publish_file_location(done=state, successful=state)
|
self._publish_file_location(done=state)
|
||||||
return super().stage()
|
return super().stage()
|
||||||
|
|
||||||
# TODO might be useful for base class
|
# TODO might be useful for base class
|
||||||
def pre_scan(self) -> None:
|
def pre_scan(self) -> None:
|
||||||
""" " Pre_scan gets executed right before"""
|
"""Pre_scan is an (optional) function that is executed by BEC just before the scan core
|
||||||
|
|
||||||
|
For the pilatus detector, it is used to arm the detector for the acquisition,
|
||||||
|
because the detector times out after ˜7-8seconds without seeing a trigger.
|
||||||
|
"""
|
||||||
self._arm_acquisition()
|
self._arm_acquisition()
|
||||||
|
|
||||||
def _arm_acquisition(self) -> None:
|
def _arm_acquisition(self) -> None:
|
||||||
self.acquire()
|
self.cam.acquire.put(1)
|
||||||
|
# TODO check if sleep of 1s is needed, could be that less is enough
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def _publish_file_location(self, done: bool = False, successful: bool = 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
|
||||||
|
|
||||||
def _publish_file_location(self, done=False, successful=False) -> None:
|
|
||||||
"""Publish the filepath to REDIS
|
|
||||||
First msg for file writer and the second one for other listeners (e.g. radial integ)
|
|
||||||
"""
|
"""
|
||||||
pipe = self._producer.pipeline()
|
pipe = self._producer.pipeline()
|
||||||
|
if successful is None:
|
||||||
|
msg = BECMessage.FileMessage(file_path=self.filepath, done=done)
|
||||||
|
else:
|
||||||
msg = BECMessage.FileMessage(file_path=self.filepath, done=done, successful=successful)
|
msg = BECMessage.FileMessage(file_path=self.filepath, done=done, successful=successful)
|
||||||
self._producer.set_and_publish(
|
self._producer.set_and_publish(
|
||||||
MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), pipe=pipe
|
MessageEndpoints.public_file(self.scaninfo.scanID, self.name), msg.dumps(), pipe=pipe
|
||||||
)
|
)
|
||||||
self._producer.set_and_publish(
|
self._producer.set_and_publish(
|
||||||
MessageEndpoints.file_event(self.name), msg.dumps(), pip=pipe
|
MessageEndpoints.file_event(self.name), msg.dumps(), pipe=pipe
|
||||||
)
|
)
|
||||||
pipe.execute()
|
pipe.execute()
|
||||||
|
|
||||||
@ -441,43 +513,30 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
- _stop_det
|
- _stop_det
|
||||||
- _stop_file_writer
|
- _stop_file_writer
|
||||||
"""
|
"""
|
||||||
|
timer = 0
|
||||||
|
sleep_time = 0.1
|
||||||
|
# TODO this is a workaround at the moment which relies on the status of the mcs device
|
||||||
while True:
|
while True:
|
||||||
if self.device_manager.devices.mcs.obj._staged != Staged.yes:
|
if self.device_manager.devices.mcs.obj._staged != Staged.yes:
|
||||||
break
|
break
|
||||||
time.sleep(0.1)
|
if self._stopped == True:
|
||||||
# TODO implement a waiting function or not
|
break
|
||||||
# time.sleep(2)
|
time.sleep(sleep_time)
|
||||||
# timer = 0
|
timer = timer + sleep_time
|
||||||
# while True:
|
if timer > 5:
|
||||||
# # rtr = self.cam.status_message_camserver.get()
|
self._stopped == True
|
||||||
# #if self.cam.acquire.get() == 0 and rtr == "Camserver returned OK":
|
|
||||||
# # if rtr == "Camserver returned OK":
|
|
||||||
# # break
|
|
||||||
# if self._stopped == True:
|
|
||||||
# break
|
|
||||||
# time.sleep(0.1)
|
|
||||||
# timer += 0.1
|
|
||||||
# if timer > 5:
|
|
||||||
# self._close_file_writer()
|
|
||||||
# self._stop_file_writer()
|
|
||||||
# self._stopped == True
|
|
||||||
# # raise PilatusTimeoutError(
|
|
||||||
# # f"Pilatus timeout with detector state {self.cam.acquire.get()} and camserver return status: {rtr} "
|
|
||||||
# # )
|
|
||||||
|
|
||||||
self._stop_det()
|
self._stop_det()
|
||||||
self._stop_file_writer()
|
self._stop_file_writer()
|
||||||
# TODO explore if sleep is needed
|
# TODO explore if sleep is needed
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
self._close_file_writer()
|
self._close_file_writer()
|
||||||
|
raise PilatusTimeoutError(f"Timeout waiting for mcs device to unstage")
|
||||||
|
|
||||||
def acquire(self) -> None:
|
self._stop_det()
|
||||||
"""Start acquisition in software trigger mode,
|
self._stop_file_writer()
|
||||||
or arm the detector in hardware of the detector
|
# TODO explore if sleep time is needed
|
||||||
"""
|
time.sleep(0.5)
|
||||||
self.cam.acquire.put(1)
|
self._close_file_writer()
|
||||||
# TODO check if sleep of 1s is needed, could be that less is enough
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
def _stop_det(self) -> None:
|
def _stop_det(self) -> None:
|
||||||
"""Stop the detector"""
|
"""Stop the detector"""
|
||||||
@ -494,4 +553,4 @@ class PilatusCsaxs(DetectorBase):
|
|||||||
|
|
||||||
# Automatically connect to test environmenr if directly invoked
|
# Automatically connect to test environmenr if directly invoked
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
pilatus_2 = PilatusCsaxs(name="pilatus_2", prefix="X12SA-ES-PILATUS300K:", sim_mode=True)
|
pilatus_2 = PilatuscSAXS(name="pilatus_2", prefix="X12SA-ES-PILATUS300K:", sim_mode=True)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from bec_lib.core import bec_logger
|
from bec_lib.core import bec_logger
|
||||||
|
from bec_lib.core.devicemanager import DeviceContainer
|
||||||
|
from bec_lib.core.tests.utils import ProducerMock
|
||||||
|
|
||||||
from ophyd import Signal, Kind
|
from ophyd import Signal, Kind
|
||||||
|
|
||||||
@ -11,33 +13,88 @@ logger = bec_logger.logger
|
|||||||
DEFAULT_EPICSSIGNAL_VALUE = object()
|
DEFAULT_EPICSSIGNAL_VALUE = object()
|
||||||
|
|
||||||
|
|
||||||
class MockProducer:
|
# TODO maybe specify here that this DeviceMock is for usage in the DeviceServer
|
||||||
def set_and_publish(self, endpoint: str, msgdump: str):
|
class DeviceMock:
|
||||||
logger.info(f"BECMessage to {endpoint} with msg dump {msgdump}")
|
def __init__(self, name: str, value: float = 0.0):
|
||||||
|
self.name = name
|
||||||
|
self.read_buffer = value
|
||||||
class MockDeviceManager:
|
self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None}
|
||||||
def __init__(self) -> None:
|
self._enabled_set = True
|
||||||
self.devices = devices()
|
self._enabled = True
|
||||||
|
|
||||||
|
|
||||||
class OphydObject:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.name = "mock_mokev"
|
|
||||||
self.obj = mokev()
|
|
||||||
|
|
||||||
|
|
||||||
class devices:
|
|
||||||
def __init__(self):
|
|
||||||
self.mokev = OphydObject()
|
|
||||||
|
|
||||||
|
|
||||||
class mokev:
|
|
||||||
def __init__(self):
|
|
||||||
self.name = "mock_mokev"
|
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
return {self.name: {"value": 16.0, "timestamp": time.time()}}
|
return {self.name: {"value": self.read_buffer}}
|
||||||
|
|
||||||
|
def readback(self):
|
||||||
|
return self.read_buffer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled_set(self) -> bool:
|
||||||
|
return self._enabled_set
|
||||||
|
|
||||||
|
@enabled_set.setter
|
||||||
|
def enabled_set(self, val: bool):
|
||||||
|
self._enabled_set = val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self) -> bool:
|
||||||
|
return self._enabled
|
||||||
|
|
||||||
|
@enabled.setter
|
||||||
|
def enabled(self, val: bool):
|
||||||
|
self._enabled = val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_parameter(self):
|
||||||
|
return self._config["userParameter"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def obj(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class DMMock:
|
||||||
|
"""Mock for DeviceManager
|
||||||
|
|
||||||
|
The mocked DeviceManager creates a device containert and a producer.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.devices = DeviceContainer()
|
||||||
|
self.producer = ProducerMock()
|
||||||
|
|
||||||
|
def add_device(self, name: str, value: float = 0.0):
|
||||||
|
self.devices[name] = DeviceMock(name, value)
|
||||||
|
|
||||||
|
|
||||||
|
# class MockProducer:
|
||||||
|
# def set_and_publish(self, endpoint: str, msgdump: str):
|
||||||
|
# logger.info(f"BECMessage to {endpoint} with msg dump {msgdump}")
|
||||||
|
|
||||||
|
|
||||||
|
# class MockDeviceManager:
|
||||||
|
# def __init__(self) -> None:
|
||||||
|
# self.devices = devices()
|
||||||
|
|
||||||
|
|
||||||
|
# class OphydObject:
|
||||||
|
# def __init__(self) -> None:
|
||||||
|
# self.name = "mock_mokev"
|
||||||
|
# self.obj = mokev()
|
||||||
|
|
||||||
|
|
||||||
|
# class devices:
|
||||||
|
# def __init__(self):
|
||||||
|
# self.mokev = OphydObject()
|
||||||
|
|
||||||
|
|
||||||
|
# class mokev:
|
||||||
|
# def __init__(self):
|
||||||
|
# self.name = "mock_mokev"
|
||||||
|
|
||||||
|
# def read(self):
|
||||||
|
# return {self.name: {"value": 16.0, "timestamp": time.time()}}
|
||||||
|
|
||||||
|
|
||||||
class ConfigSignal(Signal):
|
class ConfigSignal(Signal):
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
from bluesky import RunEngine
|
# from bluesky import RunEngine
|
||||||
from bluesky.plans import grid_scan
|
# from bluesky.plans import grid_scan
|
||||||
from bluesky.callbacks.best_effort import BestEffortCallback
|
# from bluesky.callbacks.best_effort import BestEffortCallback
|
||||||
from bluesky.callbacks.mpl_plotting import LivePlot
|
# from bluesky.callbacks.mpl_plotting import LivePlot
|
||||||
|
|
||||||
|
|
||||||
RE = RunEngine({})
|
# RE = RunEngine({})
|
||||||
|
|
||||||
from bluesky.callbacks.best_effort import BestEffortCallback
|
# from bluesky.callbacks.best_effort import BestEffortCallback
|
||||||
|
|
||||||
bec = BestEffortCallback()
|
# bec = BestEffortCallback()
|
||||||
|
|
||||||
# Send all metadata/data captured to the BestEffortCallback.
|
# # Send all metadata/data captured to the BestEffortCallback.
|
||||||
RE.subscribe(bec)
|
# RE.subscribe(bec)
|
||||||
# RE.subscribe(dummy)
|
# # RE.subscribe(dummy)
|
||||||
|
|
||||||
# RE(grid_scan(dets, motor1, -10, 10, 10, motor2, -10, 10, 10))
|
# # RE(grid_scan(dets, motor1, -10, 10, 10, motor2, -10, 10, 10))
|
||||||
|
505
tests/test_eiger9m_csaxs.py
Normal file
505
tests/test_eiger9m_csaxs.py
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import ophyd
|
||||||
|
|
||||||
|
from bec_lib.core import BECMessage, MessageEndpoints
|
||||||
|
from ophyd_devices.epics.devices.eiger9m_csaxs import Eiger9McSAXS
|
||||||
|
|
||||||
|
from tests.utils import DMMock, MockPV
|
||||||
|
|
||||||
|
|
||||||
|
def patch_dual_pvs(device):
|
||||||
|
for walk in device.walk_signals():
|
||||||
|
if not hasattr(walk.item, "_read_pv"):
|
||||||
|
continue
|
||||||
|
if not hasattr(walk.item, "_write_pv"):
|
||||||
|
continue
|
||||||
|
if walk.item._read_pv.pvname.endswith("_RBV"):
|
||||||
|
walk.item._read_pv = walk.item._write_pv
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def mock_det():
|
||||||
|
name = "eiger"
|
||||||
|
prefix = "X12SA-ES-EIGER9M:"
|
||||||
|
sim_mode = False
|
||||||
|
dm = DMMock()
|
||||||
|
with mock.patch.object(dm, "producer"):
|
||||||
|
with mock.patch(
|
||||||
|
"ophyd_devices.epics.devices.eiger9m_csaxs.FileWriterMixin"
|
||||||
|
) as filemixin, mock.patch(
|
||||||
|
"ophyd_devices.epics.devices.eiger9m_csaxs.Eiger9McSAXS._update_service_config"
|
||||||
|
) as mock_service_config:
|
||||||
|
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||||
|
mock_cl.get_pv = MockPV
|
||||||
|
with mock.patch.object(Eiger9McSAXS, "_init"):
|
||||||
|
det = Eiger9McSAXS(
|
||||||
|
name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode
|
||||||
|
)
|
||||||
|
patch_dual_pvs(det)
|
||||||
|
yield det
|
||||||
|
|
||||||
|
|
||||||
|
def test_init():
|
||||||
|
"""Test the _init function:"""
|
||||||
|
name = "eiger"
|
||||||
|
prefix = "X12SA-ES-EIGER9M:"
|
||||||
|
sim_mode = False
|
||||||
|
dm = DMMock()
|
||||||
|
with mock.patch.object(dm, "producer"):
|
||||||
|
with mock.patch(
|
||||||
|
"ophyd_devices.epics.devices.eiger9m_csaxs.FileWriterMixin"
|
||||||
|
) as filemixin, mock.patch(
|
||||||
|
"ophyd_devices.epics.devices.eiger9m_csaxs.Eiger9McSAXS._update_service_config"
|
||||||
|
) as mock_service_config:
|
||||||
|
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||||
|
mock_cl.get_pv = MockPV
|
||||||
|
with mock.patch.object(
|
||||||
|
Eiger9McSAXS, "_default_parameter"
|
||||||
|
) as mock_default, mock.patch.object(
|
||||||
|
Eiger9McSAXS, "_init_detector"
|
||||||
|
) as mock_init_det, mock.patch.object(
|
||||||
|
Eiger9McSAXS, "_init_filewriter"
|
||||||
|
) as mock_init_fw:
|
||||||
|
Eiger9McSAXS(name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode)
|
||||||
|
mock_default.assert_called_once()
|
||||||
|
mock_init_det.assert_called_once()
|
||||||
|
mock_init_fw.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"trigger_source, detector_state, expected_exception",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_init_detector(
|
||||||
|
mock_det,
|
||||||
|
trigger_source,
|
||||||
|
detector_state,
|
||||||
|
expected_exception,
|
||||||
|
):
|
||||||
|
"""Test the _init function:
|
||||||
|
|
||||||
|
This includes testing the functions:
|
||||||
|
- _init_detector
|
||||||
|
- _stop_det
|
||||||
|
- _set_trigger
|
||||||
|
--> Testing the filewriter is done in test_init_filewriter
|
||||||
|
|
||||||
|
Validation upon setting the correct PVs
|
||||||
|
|
||||||
|
"""
|
||||||
|
mock_det.cam.detector_state._read_pv.mock_data = detector_state
|
||||||
|
if expected_exception:
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
mock_det._init_detector()
|
||||||
|
else:
|
||||||
|
mock_det._init_detector() # call the method you want to test
|
||||||
|
assert mock_det.cam.acquire.get() == 0
|
||||||
|
assert mock_det.cam.detector_state.get() == detector_state
|
||||||
|
assert mock_det.cam.trigger_mode.get() == trigger_source
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"readout_time, expected_value",
|
||||||
|
[
|
||||||
|
(1e-3, 3e-3),
|
||||||
|
(3e-3, 3e-3),
|
||||||
|
(5e-3, 5e-3),
|
||||||
|
(None, 3e-3),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_update_readout_time(mock_det, readout_time, expected_value):
|
||||||
|
if readout_time is None:
|
||||||
|
mock_det._update_readout_time()
|
||||||
|
assert mock_det.readout_time == expected_value
|
||||||
|
else:
|
||||||
|
mock_det.scaninfo.readout_time = readout_time
|
||||||
|
mock_det._update_readout_time()
|
||||||
|
assert mock_det.readout_time == expected_value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"eacc, exp_url, daq_status, daq_cfg, expected_exception",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"e12345",
|
||||||
|
"http://xbl-daq-29:5000",
|
||||||
|
{"state": "READY"},
|
||||||
|
{"writer_user_id": 12543},
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"e12345",
|
||||||
|
"http://xbl-daq-29:5000",
|
||||||
|
{"state": "READY"},
|
||||||
|
{"writer_user_id": 15421},
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"e12345",
|
||||||
|
"http://xbl-daq-29:5000",
|
||||||
|
{"state": "BUSY"},
|
||||||
|
{"writer_user_id": 15421},
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"e12345",
|
||||||
|
"http://xbl-daq-29:5000",
|
||||||
|
{"state": "READY"},
|
||||||
|
{"writer_ud": 12345},
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_init_filewriter(mock_det, eacc, exp_url, daq_status, daq_cfg, expected_exception):
|
||||||
|
"""Test _init_filewriter (std daq in this case)
|
||||||
|
|
||||||
|
This includes testing the functions:
|
||||||
|
|
||||||
|
- _update_service_config
|
||||||
|
|
||||||
|
Validation upon checking set values in mocked std_daq instance
|
||||||
|
"""
|
||||||
|
with mock.patch("ophyd_devices.epics.devices.eiger9m_csaxs.StdDaqClient") as mock_std_daq:
|
||||||
|
instance = mock_std_daq.return_value
|
||||||
|
instance.stop_writer.return_value = None
|
||||||
|
instance.get_status.return_value = daq_status
|
||||||
|
instance.get_config.return_value = daq_cfg
|
||||||
|
mock_det.scaninfo.username = eacc
|
||||||
|
# scaninfo.username.return_value = eacc
|
||||||
|
if expected_exception:
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
mock_det._init_filewriter()
|
||||||
|
else:
|
||||||
|
mock_det._init_filewriter()
|
||||||
|
|
||||||
|
assert mock_det.std_rest_server_url == exp_url
|
||||||
|
instance.stop_writer.assert_called_once()
|
||||||
|
instance.get_status.assert_called()
|
||||||
|
instance.set_config.assert_called_once_with(daq_cfg)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scaninfo, daq_status, daq_cfg, detector_state, stopped, expected_exception",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"eacc": "e12345",
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
"filepath": "test.h5",
|
||||||
|
"scanID": "123",
|
||||||
|
"mokev": 12.4,
|
||||||
|
},
|
||||||
|
{"state": "READY"},
|
||||||
|
{"writer_user_id": 12543},
|
||||||
|
5,
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"eacc": "e12345",
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
"filepath": "test.h5",
|
||||||
|
"scanID": "123",
|
||||||
|
"mokev": 12.4,
|
||||||
|
},
|
||||||
|
{"state": "BUSY"},
|
||||||
|
{"writer_user_id": 15421},
|
||||||
|
5,
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"eacc": "e12345",
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
"filepath": "test.h5",
|
||||||
|
"scanID": "123",
|
||||||
|
"mokev": 18.4,
|
||||||
|
},
|
||||||
|
{"state": "READY"},
|
||||||
|
{"writer_user_id": 12345},
|
||||||
|
4,
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_stage(
|
||||||
|
mock_det,
|
||||||
|
scaninfo,
|
||||||
|
daq_status,
|
||||||
|
daq_cfg,
|
||||||
|
detector_state,
|
||||||
|
stopped,
|
||||||
|
expected_exception,
|
||||||
|
):
|
||||||
|
with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object(
|
||||||
|
Eiger9McSAXS, "_publish_file_location"
|
||||||
|
) as mock_publish_file_location:
|
||||||
|
mock_std_daq.stop_writer.return_value = None
|
||||||
|
mock_std_daq.get_status.return_value = daq_status
|
||||||
|
mock_std_daq.get_config.return_value = daq_cfg
|
||||||
|
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||||
|
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||||
|
mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"]
|
||||||
|
# TODO consider putting energy as variable in scaninfo
|
||||||
|
mock_det.device_manager.add_device("mokev", value=12.4)
|
||||||
|
mock_det.cam.beam_energy.put(scaninfo["mokev"])
|
||||||
|
mock_det._stopped = stopped
|
||||||
|
mock_det.cam.detector_state._read_pv.mock_data = detector_state
|
||||||
|
with mock.patch.object(mock_det, "_prep_file_writer") as mock_prep_fw:
|
||||||
|
mock_det.filepath = scaninfo["filepath"]
|
||||||
|
if expected_exception:
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
mock_det.stage()
|
||||||
|
else:
|
||||||
|
mock_det.stage()
|
||||||
|
mock_prep_fw.assert_called_once()
|
||||||
|
# Check _prep_det
|
||||||
|
assert mock_det.cam.num_images.get() == int(
|
||||||
|
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||||
|
)
|
||||||
|
assert mock_det.cam.num_frames.get() == 1
|
||||||
|
|
||||||
|
mock_publish_file_location.assert_called_with(done=False)
|
||||||
|
assert mock_det.cam.acquire.get() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scaninfo, daq_status, expected_exception",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"eacc": "e12345",
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
"filepath": "test.h5",
|
||||||
|
"scanID": "123",
|
||||||
|
},
|
||||||
|
{"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}},
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"eacc": "e12345",
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
"filepath": "test.h5",
|
||||||
|
"scanID": "123",
|
||||||
|
},
|
||||||
|
{"state": "BUSY", "acquisition": {"state": "WAITING_IMAGES"}},
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"eacc": "e12345",
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
"filepath": "test.h5",
|
||||||
|
"scanID": "123",
|
||||||
|
},
|
||||||
|
{"state": "BUSY", "acquisition": {"state": "ERROR"}},
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_prep_file_writer(mock_det, scaninfo, daq_status, expected_exception):
|
||||||
|
with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object(
|
||||||
|
mock_det, "_filepath_exists"
|
||||||
|
) as mock_file_path_exists, mock.patch.object(
|
||||||
|
mock_det, "_stop_file_writer"
|
||||||
|
) as mock_stop_file_writer, mock.patch.object(
|
||||||
|
mock_det, "scaninfo"
|
||||||
|
) as mock_scaninfo:
|
||||||
|
# mock_det = eiger_factory(name, prefix, sim_mode)
|
||||||
|
mock_det.std_client = mock_std_daq
|
||||||
|
mock_std_daq.start_writer_async.return_value = None
|
||||||
|
mock_std_daq.get_status.return_value = daq_status
|
||||||
|
mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"]
|
||||||
|
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||||
|
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||||
|
|
||||||
|
if expected_exception:
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
mock_det._prep_file_writer()
|
||||||
|
mock_file_path_exists.assert_called_once()
|
||||||
|
assert mock_stop_file_writer.call_count == 2
|
||||||
|
|
||||||
|
else:
|
||||||
|
mock_det._prep_file_writer()
|
||||||
|
mock_file_path_exists.assert_called_once()
|
||||||
|
mock_stop_file_writer.assert_called_once()
|
||||||
|
|
||||||
|
daq_writer_call = {
|
||||||
|
"output_file": scaninfo["filepath"],
|
||||||
|
"n_images": int(scaninfo["num_points"] * scaninfo["frames_per_trigger"]),
|
||||||
|
}
|
||||||
|
mock_std_daq.start_writer_async.assert_called_with(daq_writer_call)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"stopped, expected_exception",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
True,
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_unstage(
|
||||||
|
mock_det,
|
||||||
|
stopped,
|
||||||
|
expected_exception,
|
||||||
|
):
|
||||||
|
with mock.patch.object(mock_det, "_finished") as mock_finished, mock.patch.object(
|
||||||
|
mock_det, "_publish_file_location"
|
||||||
|
) as mock_publish_file_location:
|
||||||
|
mock_det._stopped = stopped
|
||||||
|
if expected_exception:
|
||||||
|
mock_det.unstage()
|
||||||
|
assert mock_det._stopped == True
|
||||||
|
else:
|
||||||
|
mock_det.unstage()
|
||||||
|
mock_finished.assert_called_once()
|
||||||
|
mock_publish_file_location.assert_called_with(done=True, successful=True)
|
||||||
|
assert mock_det._stopped == False
|
||||||
|
|
||||||
|
|
||||||
|
def test_stop_fw(mock_det):
|
||||||
|
with mock.patch.object(mock_det, "std_client") as mock_std_daq:
|
||||||
|
mock_std_daq.stop_writer.return_value = None
|
||||||
|
mock_det.std_client = mock_std_daq
|
||||||
|
mock_det._stop_file_writer()
|
||||||
|
mock_std_daq.stop_writer.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scaninfo",
|
||||||
|
[
|
||||||
|
({"filepath": "test.h5", "successful": True, "done": False, "scanID": "123"}),
|
||||||
|
({"filepath": "test.h5", "successful": False, "done": True, "scanID": "123"}),
|
||||||
|
({"filepath": "test.h5", "successful": None, "done": True, "scanID": "123"}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_publish_file_location(mock_det, scaninfo):
|
||||||
|
mock_det.scaninfo.scanID = scaninfo["scanID"]
|
||||||
|
mock_det.filepath = scaninfo["filepath"]
|
||||||
|
mock_det._publish_file_location(done=scaninfo["done"], successful=scaninfo["successful"])
|
||||||
|
if scaninfo["successful"] is None:
|
||||||
|
msg = BECMessage.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps()
|
||||||
|
else:
|
||||||
|
msg = BECMessage.FileMessage(
|
||||||
|
file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"]
|
||||||
|
).dumps()
|
||||||
|
expected_calls = [
|
||||||
|
mock.call(
|
||||||
|
MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name),
|
||||||
|
msg,
|
||||||
|
pipe=mock_det._producer.pipeline.return_value,
|
||||||
|
),
|
||||||
|
mock.call(
|
||||||
|
MessageEndpoints.file_event(mock_det.name),
|
||||||
|
msg,
|
||||||
|
pipe=mock_det._producer.pipeline.return_value,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
assert mock_det._producer.set_and_publish.call_args_list == expected_calls
|
||||||
|
|
||||||
|
|
||||||
|
def test_stop(mock_det):
|
||||||
|
with mock.patch.object(mock_det, "_stop_det") as mock_stop_det, mock.patch.object(
|
||||||
|
mock_det, "_stop_file_writer"
|
||||||
|
) as mock_stop_file_writer:
|
||||||
|
mock_det.stop()
|
||||||
|
mock_stop_det.assert_called_once()
|
||||||
|
mock_stop_file_writer.assert_called_once()
|
||||||
|
assert mock_det._stopped == True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"stopped, scaninfo, cam_state, daq_status, expected_exception",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
False,
|
||||||
|
{
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 4,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
{"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 2000}}},
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
False,
|
||||||
|
{
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 4,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
{"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 1999}}},
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
False,
|
||||||
|
{
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
{"acquisition": {"state": "READY", "stats": {"n_write_completed": 500}}},
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
False,
|
||||||
|
{
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
{"acquisition": {"state": "FINISHED", "stats": {"n_write_completed": 500}}},
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_finished(mock_det, stopped, cam_state, daq_status, scaninfo, expected_exception):
|
||||||
|
with mock.patch.object(mock_det, "std_client") as mock_std_daq, mock.patch.object(
|
||||||
|
mock_det, "_stop_file_writer"
|
||||||
|
) as mock_stop_file_friter, mock.patch.object(mock_det, "_stop_det") as mock_stop_det:
|
||||||
|
mock_std_daq.get_status.return_value = daq_status
|
||||||
|
mock_det.cam.acquire._read_pv.mock_state = cam_state
|
||||||
|
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||||
|
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||||
|
if expected_exception:
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
mock_det._finished()
|
||||||
|
assert mock_det._stopped == stopped
|
||||||
|
mock_stop_file_friter.assert_called()
|
||||||
|
mock_stop_det.assert_called_once()
|
||||||
|
else:
|
||||||
|
mock_det._finished()
|
||||||
|
if stopped:
|
||||||
|
assert mock_det._stopped == stopped
|
||||||
|
|
||||||
|
mock_stop_file_friter.assert_called()
|
||||||
|
mock_stop_det.assert_called_once()
|
503
tests/test_pilatus_csaxs.py
Normal file
503
tests/test_pilatus_csaxs.py
Normal file
@ -0,0 +1,503 @@
|
|||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import ophyd
|
||||||
|
|
||||||
|
from bec_lib.core import BECMessage, MessageEndpoints
|
||||||
|
from ophyd_devices.epics.devices.pilatus_csaxs import PilatuscSAXS
|
||||||
|
|
||||||
|
from tests.utils import DMMock, MockPV
|
||||||
|
|
||||||
|
|
||||||
|
def patch_dual_pvs(device):
|
||||||
|
for walk in device.walk_signals():
|
||||||
|
if not hasattr(walk.item, "_read_pv"):
|
||||||
|
continue
|
||||||
|
if not hasattr(walk.item, "_write_pv"):
|
||||||
|
continue
|
||||||
|
if walk.item._read_pv.pvname.endswith("_RBV"):
|
||||||
|
walk.item._read_pv = walk.item._write_pv
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def mock_det():
|
||||||
|
name = "pilatus"
|
||||||
|
prefix = "X12SA-ES-PILATUS300K:"
|
||||||
|
sim_mode = False
|
||||||
|
dm = DMMock()
|
||||||
|
with mock.patch.object(dm, "producer"):
|
||||||
|
with mock.patch(
|
||||||
|
"ophyd_devices.epics.devices.pilatus_csaxs.FileWriterMixin"
|
||||||
|
) as filemixin, mock.patch(
|
||||||
|
"ophyd_devices.epics.devices.pilatus_csaxs.PilatuscSAXS._update_service_config"
|
||||||
|
) as mock_service_config:
|
||||||
|
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||||
|
mock_cl.get_pv = MockPV
|
||||||
|
with mock.patch.object(PilatuscSAXS, "_init"):
|
||||||
|
det = PilatuscSAXS(
|
||||||
|
name=name, prefix=prefix, device_manager=dm, sim_mode=sim_mode
|
||||||
|
)
|
||||||
|
patch_dual_pvs(det)
|
||||||
|
yield det
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"trigger_source, detector_state",
|
||||||
|
[
|
||||||
|
(1, 0),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
# TODO rewrite this one, write test for init_detector, init_filewriter is tested
|
||||||
|
def test_init_detector(
|
||||||
|
mock_det,
|
||||||
|
trigger_source,
|
||||||
|
detector_state,
|
||||||
|
):
|
||||||
|
"""Test the _init function:
|
||||||
|
|
||||||
|
This includes testing the functions:
|
||||||
|
- _init_detector
|
||||||
|
- _stop_det
|
||||||
|
- _set_trigger
|
||||||
|
--> Testing the filewriter is done in test_init_filewriter
|
||||||
|
|
||||||
|
Validation upon setting the correct PVs
|
||||||
|
|
||||||
|
"""
|
||||||
|
mock_det._init_detector() # call the method you want to test
|
||||||
|
assert mock_det.cam.acquire.get() == detector_state
|
||||||
|
assert mock_det.cam.trigger_mode.get() == trigger_source
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scaninfo, stopped, expected_exception",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"eacc": "e12345",
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
"filepath": "test.h5",
|
||||||
|
"scanID": "123",
|
||||||
|
"mokev": 12.4,
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"eacc": "e12345",
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
"filepath": "test.h5",
|
||||||
|
"scanID": "123",
|
||||||
|
"mokev": 12.4,
|
||||||
|
},
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_stage(
|
||||||
|
mock_det,
|
||||||
|
scaninfo,
|
||||||
|
stopped,
|
||||||
|
expected_exception,
|
||||||
|
):
|
||||||
|
with mock.patch.object(mock_det, "_publish_file_location") as mock_publish_file_location:
|
||||||
|
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||||
|
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||||
|
mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"]
|
||||||
|
# TODO consider putting energy as variable in scaninfo
|
||||||
|
mock_det.device_manager.add_device("mokev", value=12.4)
|
||||||
|
mock_det._stopped = stopped
|
||||||
|
with mock.patch.object(mock_det, "_prep_file_writer") as mock_prep_fw, mock.patch.object(
|
||||||
|
mock_det, "_update_readout_time"
|
||||||
|
) as mock_update_readout_time:
|
||||||
|
mock_det.filepath = scaninfo["filepath"]
|
||||||
|
if expected_exception:
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
mock_det.stage()
|
||||||
|
else:
|
||||||
|
mock_det.stage()
|
||||||
|
mock_prep_fw.assert_called_once()
|
||||||
|
mock_update_readout_time.assert_called()
|
||||||
|
# Check _prep_det
|
||||||
|
assert mock_det.cam.num_images.get() == int(
|
||||||
|
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||||
|
)
|
||||||
|
assert mock_det.cam.num_frames.get() == 1
|
||||||
|
|
||||||
|
mock_publish_file_location.assert_called_with(done=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pre_scan(mock_det):
|
||||||
|
mock_det.pre_scan()
|
||||||
|
assert mock_det.cam.acquire.get() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"readout_time, expected_value",
|
||||||
|
[
|
||||||
|
(1e-3, 3e-3),
|
||||||
|
(3e-3, 3e-3),
|
||||||
|
(5e-3, 5e-3),
|
||||||
|
(None, 3e-3),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_update_readout_time(mock_det, readout_time, expected_value):
|
||||||
|
if readout_time is None:
|
||||||
|
mock_det._update_readout_time()
|
||||||
|
assert mock_det.readout_time == expected_value
|
||||||
|
else:
|
||||||
|
mock_det.scaninfo.readout_time = readout_time
|
||||||
|
mock_det._update_readout_time()
|
||||||
|
assert mock_det.readout_time == expected_value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scaninfo",
|
||||||
|
[
|
||||||
|
({"filepath": "test.h5", "successful": True, "done": False, "scanID": "123"}),
|
||||||
|
({"filepath": "test.h5", "successful": False, "done": True, "scanID": "123"}),
|
||||||
|
({"filepath": "test.h5", "successful": None, "done": True, "scanID": "123"}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_publish_file_location(mock_det, scaninfo):
|
||||||
|
mock_det.scaninfo.scanID = scaninfo["scanID"]
|
||||||
|
mock_det.filepath = scaninfo["filepath"]
|
||||||
|
mock_det._publish_file_location(done=scaninfo["done"], successful=scaninfo["successful"])
|
||||||
|
if scaninfo["successful"] is None:
|
||||||
|
msg = BECMessage.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"]).dumps()
|
||||||
|
else:
|
||||||
|
msg = BECMessage.FileMessage(
|
||||||
|
file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"]
|
||||||
|
).dumps()
|
||||||
|
expected_calls = [
|
||||||
|
mock.call(
|
||||||
|
MessageEndpoints.public_file(scaninfo["scanID"], mock_det.name),
|
||||||
|
msg,
|
||||||
|
pipe=mock_det._producer.pipeline.return_value,
|
||||||
|
),
|
||||||
|
mock.call(
|
||||||
|
MessageEndpoints.file_event(mock_det.name),
|
||||||
|
msg,
|
||||||
|
pipe=mock_det._producer.pipeline.return_value,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
assert mock_det._producer.set_and_publish.call_args_list == expected_calls
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"requests_state, expected_exception, url",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
"http://x12sa-pd-2:8080/stream/pilatus_2",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
"http://x12sa-pd-2:8080/stream/pilatus_2",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_close_file_writer(mock_det, requests_state, expected_exception, url):
|
||||||
|
with mock.patch.object(mock_det, "_send_requests_delete") as mock_send_requests_delete:
|
||||||
|
instance = mock_send_requests_delete.return_value
|
||||||
|
instance.ok = requests_state
|
||||||
|
if expected_exception:
|
||||||
|
mock_det._close_file_writer()
|
||||||
|
mock_send_requests_delete.assert_called_once_with(url=url)
|
||||||
|
instance.raise_for_status.called_once()
|
||||||
|
else:
|
||||||
|
mock_det._close_file_writer()
|
||||||
|
mock_send_requests_delete.assert_called_once_with(url=url)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"requests_state, expected_exception, url",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
"http://xbl-daq-34:8091/pilatus_2/stop",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
"http://xbl-daq-34:8091/pilatus_2/stop",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_stop_file_writer(mock_det, requests_state, expected_exception, url):
|
||||||
|
with mock.patch.object(mock_det, "_send_requests_put") as mock_send_requests_put:
|
||||||
|
instance = mock_send_requests_put.return_value
|
||||||
|
instance.ok = requests_state
|
||||||
|
instance.raise_for_status.side_effect = Exception
|
||||||
|
if expected_exception:
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
mock_det._stop_file_writer()
|
||||||
|
mock_send_requests_put.assert_called_once_with(url=url)
|
||||||
|
instance.raise_for_status.called_once()
|
||||||
|
else:
|
||||||
|
mock_det._stop_file_writer()
|
||||||
|
mock_send_requests_put.assert_called_once_with(url=url)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scaninfo, data_msgs, urls, requests_state, expected_exception",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"filepath_raw": "pilatus_2.h5",
|
||||||
|
"eacc": "e12345",
|
||||||
|
"scan_number": 1000,
|
||||||
|
"scan_directory": "S00000_00999",
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
"headers": {"Content-Type": "application/json", "Accept": "application/json"},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"source": [
|
||||||
|
{
|
||||||
|
"searchPath": "/",
|
||||||
|
"searchPattern": "glob:*.cbf",
|
||||||
|
"destinationPath": "/sls/X12SA/data/e12345/Data10/pilatus_2/S00000_00999",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[
|
||||||
|
"zmqWriter",
|
||||||
|
"e12345",
|
||||||
|
{
|
||||||
|
"addr": "tcp://x12sa-pd-2:8888",
|
||||||
|
"dst": ["file"],
|
||||||
|
"numFrm": 500,
|
||||||
|
"timeout": 2000,
|
||||||
|
"ifType": "PULL",
|
||||||
|
"user": "e12345",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"zmqWriter",
|
||||||
|
"e12345",
|
||||||
|
{
|
||||||
|
"frmCnt": 500,
|
||||||
|
"timeout": 2000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"http://x12sa-pd-2:8080/stream/pilatus_2",
|
||||||
|
"http://xbl-daq-34:8091/pilatus_2/run",
|
||||||
|
"http://xbl-daq-34:8091/pilatus_2/wait",
|
||||||
|
],
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"filepath_raw": "pilatus_2.h5",
|
||||||
|
"eacc": "e12345",
|
||||||
|
"scan_number": 1000,
|
||||||
|
"scan_directory": "S00000_00999",
|
||||||
|
"num_points": 500,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
"headers": {"Content-Type": "application/json", "Accept": "application/json"},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"source": [
|
||||||
|
{
|
||||||
|
"searchPath": "/",
|
||||||
|
"searchPattern": "glob:*.cbf",
|
||||||
|
"destinationPath": "/sls/X12SA/data/e12345/Data10/pilatus_2/S00000_00999",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[
|
||||||
|
"zmqWriter",
|
||||||
|
"e12345",
|
||||||
|
{
|
||||||
|
"addr": "tcp://x12sa-pd-2:8888",
|
||||||
|
"dst": ["file"],
|
||||||
|
"numFrm": 500,
|
||||||
|
"timeout": 2000,
|
||||||
|
"ifType": "PULL",
|
||||||
|
"user": "e12345",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"zmqWriter",
|
||||||
|
"e12345",
|
||||||
|
{
|
||||||
|
"frmCnt": 500,
|
||||||
|
"timeout": 2000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"http://x12sa-pd-2:8080/stream/pilatus_2",
|
||||||
|
"http://xbl-daq-34:8091/pilatus_2/run",
|
||||||
|
"http://xbl-daq-34:8091/pilatus_2/wait",
|
||||||
|
],
|
||||||
|
False, # return of res.ok is False!
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_prep_file_writer(mock_det, scaninfo, data_msgs, urls, requests_state, expected_exception):
|
||||||
|
with mock.patch.object(
|
||||||
|
mock_det, "_close_file_writer"
|
||||||
|
) as mock_close_file_writer, mock.patch.object(
|
||||||
|
mock_det, "_stop_file_writer"
|
||||||
|
) as mock_stop_file_writer, mock.patch.object(
|
||||||
|
mock_det, "filewriter"
|
||||||
|
) as mock_filewriter, mock.patch.object(
|
||||||
|
mock_det, "_create_directory"
|
||||||
|
) as mock_create_directory, mock.patch.object(
|
||||||
|
mock_det, "_send_requests_put"
|
||||||
|
) as mock_send_requests_put:
|
||||||
|
mock_det.scaninfo.scan_number = scaninfo["scan_number"]
|
||||||
|
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||||
|
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||||
|
mock_det.scaninfo.username = scaninfo["eacc"]
|
||||||
|
mock_filewriter.compile_full_filename.return_value = scaninfo["filepath_raw"]
|
||||||
|
mock_filewriter.get_scan_directory.return_value = scaninfo["scan_directory"]
|
||||||
|
instance = mock_send_requests_put.return_value
|
||||||
|
instance.ok = requests_state
|
||||||
|
instance.raise_for_status.side_effect = Exception
|
||||||
|
|
||||||
|
if expected_exception:
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
mock_det._prep_file_writer()
|
||||||
|
mock_close_file_writer.assert_called_once()
|
||||||
|
mock_stop_file_writer.assert_called_once()
|
||||||
|
instance.raise_for_status.assert_called_once()
|
||||||
|
else:
|
||||||
|
mock_det._prep_file_writer()
|
||||||
|
|
||||||
|
mock_close_file_writer.assert_called_once()
|
||||||
|
mock_stop_file_writer.assert_called_once()
|
||||||
|
|
||||||
|
# Assert values set on detector
|
||||||
|
assert mock_det.cam.file_path.get() == "/dev/shm/zmq/"
|
||||||
|
assert (
|
||||||
|
mock_det.cam.file_name.get()
|
||||||
|
== f"{scaninfo['eacc']}_2_{scaninfo['scan_number']:05d}"
|
||||||
|
)
|
||||||
|
assert mock_det.cam.auto_increment.get() == 1
|
||||||
|
assert mock_det.cam.file_number.get() == 0
|
||||||
|
assert mock_det.cam.file_format.get() == 0
|
||||||
|
assert mock_det.cam.file_template.get() == "%s%s_%5.5d.cbf"
|
||||||
|
# Remove last / from destinationPath
|
||||||
|
mock_create_directory.assert_called_once_with(
|
||||||
|
os.path.join(data_msgs[0]["source"][0]["destinationPath"])
|
||||||
|
)
|
||||||
|
assert mock_send_requests_put.call_count == 3
|
||||||
|
|
||||||
|
calls = [
|
||||||
|
mock.call(url=url, data=data_msg, headers=scaninfo["headers"])
|
||||||
|
for url, data_msg in zip(urls, data_msgs)
|
||||||
|
]
|
||||||
|
for call, mock_call in zip(calls, mock_send_requests_put.call_args_list):
|
||||||
|
assert call == mock_call
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"stopped, expected_exception",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
True,
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_unstage(
|
||||||
|
mock_det,
|
||||||
|
stopped,
|
||||||
|
expected_exception,
|
||||||
|
):
|
||||||
|
with mock.patch.object(mock_det, "_finished") as mock_finished, mock.patch.object(
|
||||||
|
mock_det, "_publish_file_location"
|
||||||
|
) as mock_publish_file_location, mock.patch.object(
|
||||||
|
mock_det, "_start_h5converter"
|
||||||
|
) as mock_start_h5converter:
|
||||||
|
mock_det._stopped = stopped
|
||||||
|
if expected_exception:
|
||||||
|
mock_det.unstage()
|
||||||
|
assert mock_det._stopped == True
|
||||||
|
else:
|
||||||
|
mock_det.unstage()
|
||||||
|
mock_finished.assert_called_once()
|
||||||
|
mock_publish_file_location.assert_called_with(done=True, successful=True)
|
||||||
|
mock_start_h5converter.assert_called_once()
|
||||||
|
assert mock_det._stopped == False
|
||||||
|
|
||||||
|
|
||||||
|
def test_stop(mock_det):
|
||||||
|
with mock.patch.object(mock_det, "_stop_det") as mock_stop_det, mock.patch.object(
|
||||||
|
mock_det, "_stop_file_writer"
|
||||||
|
) as mock_stop_file_writer, mock.patch.object(
|
||||||
|
mock_det, "_close_file_writer"
|
||||||
|
) as mock_close_file_writer:
|
||||||
|
mock_det.stop()
|
||||||
|
mock_stop_det.assert_called_once()
|
||||||
|
mock_stop_file_writer.assert_called_once()
|
||||||
|
mock_close_file_writer.assert_called_once()
|
||||||
|
assert mock_det._stopped == True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"stopped, mcs_stage_state, expected_exception",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
False,
|
||||||
|
ophyd.Staged.no,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
True,
|
||||||
|
ophyd.Staged.no,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
False,
|
||||||
|
ophyd.Staged.yes,
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_finished(mock_det, stopped, mcs_stage_state, expected_exception):
|
||||||
|
with mock.patch.object(mock_det, "device_manager") as mock_dm, mock.patch.object(
|
||||||
|
mock_det, "_stop_file_writer"
|
||||||
|
) as mock_stop_file_friter, mock.patch.object(
|
||||||
|
mock_det, "_stop_det"
|
||||||
|
) as mock_stop_det, mock.patch.object(
|
||||||
|
mock_det, "_close_file_writer"
|
||||||
|
) as mock_close_file_writer:
|
||||||
|
mock_dm.devices.mcs.obj._staged = mcs_stage_state
|
||||||
|
mock_det._stopped = stopped
|
||||||
|
if expected_exception:
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
mock_det._finished()
|
||||||
|
assert mock_det._stopped == stopped
|
||||||
|
mock_stop_file_friter.assert_called()
|
||||||
|
mock_stop_det.assert_called_once()
|
||||||
|
mock_close_file_writer.assert_called_once()
|
||||||
|
else:
|
||||||
|
mock_det._finished()
|
||||||
|
if stopped:
|
||||||
|
assert mock_det._stopped == stopped
|
||||||
|
|
||||||
|
mock_stop_file_friter.assert_called()
|
||||||
|
mock_stop_det.assert_called_once()
|
||||||
|
mock_close_file_writer.assert_called_once()
|
249
tests/utils.py
249
tests/utils.py
@ -1,3 +1,9 @@
|
|||||||
|
from bec_lib.core.devicemanager import DeviceContainer
|
||||||
|
from bec_lib.core.tests.utils import ProducerMock
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
class SocketMock:
|
class SocketMock:
|
||||||
def __init__(self, host, port):
|
def __init__(self, host, port):
|
||||||
self.host = host
|
self.host = host
|
||||||
@ -44,3 +50,246 @@ class SocketMock:
|
|||||||
def flush_buffer(self):
|
def flush_buffer(self):
|
||||||
self.buffer_put = []
|
self.buffer_put = []
|
||||||
self.buffer_recv = ""
|
self.buffer_recv = ""
|
||||||
|
|
||||||
|
|
||||||
|
class MockPV:
|
||||||
|
"""
|
||||||
|
MockPV class
|
||||||
|
|
||||||
|
This class is used for mocking pyepics signals for testing purposes
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
_fmtsca = "<PV '%(pvname)s', count=%(count)i, type=%(typefull)s, access=%(access)s>"
|
||||||
|
_fmtarr = "<PV '%(pvname)s', count=%(count)i/%(nelm)i, type=%(typefull)s, access=%(access)s>"
|
||||||
|
_fields = (
|
||||||
|
"pvname",
|
||||||
|
"value",
|
||||||
|
"char_value",
|
||||||
|
"status",
|
||||||
|
"ftype",
|
||||||
|
"chid",
|
||||||
|
"host",
|
||||||
|
"count",
|
||||||
|
"access",
|
||||||
|
"write_access",
|
||||||
|
"read_access",
|
||||||
|
"severity",
|
||||||
|
"timestamp",
|
||||||
|
"posixseconds",
|
||||||
|
"nanoseconds",
|
||||||
|
"precision",
|
||||||
|
"units",
|
||||||
|
"enum_strs",
|
||||||
|
"upper_disp_limit",
|
||||||
|
"lower_disp_limit",
|
||||||
|
"upper_alarm_limit",
|
||||||
|
"lower_alarm_limit",
|
||||||
|
"lower_warning_limit",
|
||||||
|
"upper_warning_limit",
|
||||||
|
"upper_ctrl_limit",
|
||||||
|
"lower_ctrl_limit",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
pvname,
|
||||||
|
callback=None,
|
||||||
|
form="time",
|
||||||
|
verbose=False,
|
||||||
|
auto_monitor=None,
|
||||||
|
count=None,
|
||||||
|
connection_callback=None,
|
||||||
|
connection_timeout=None,
|
||||||
|
access_callback=None,
|
||||||
|
):
|
||||||
|
self.pvname = pvname.strip()
|
||||||
|
self.form = form.lower()
|
||||||
|
self.verbose = verbose
|
||||||
|
self._auto_monitor = auto_monitor
|
||||||
|
self.ftype = None
|
||||||
|
self.connected = True
|
||||||
|
self.connection_timeout = connection_timeout
|
||||||
|
self._user_max_count = count
|
||||||
|
|
||||||
|
if self.connection_timeout is None:
|
||||||
|
self.connection_timeout = 3
|
||||||
|
self._args = {}.fromkeys(self._fields)
|
||||||
|
self._args["pvname"] = self.pvname
|
||||||
|
self._args["count"] = count
|
||||||
|
self._args["nelm"] = -1
|
||||||
|
self._args["type"] = "unknown"
|
||||||
|
self._args["typefull"] = "unknown"
|
||||||
|
self._args["access"] = "unknown"
|
||||||
|
self._args["status"] = 0
|
||||||
|
self.connection_callbacks = []
|
||||||
|
self.mock_data = 0
|
||||||
|
|
||||||
|
if connection_callback is not None:
|
||||||
|
self.connection_callbacks = [connection_callback]
|
||||||
|
|
||||||
|
self.access_callbacks = []
|
||||||
|
if access_callback is not None:
|
||||||
|
self.access_callbacks = [access_callback]
|
||||||
|
|
||||||
|
self.callbacks = {}
|
||||||
|
self._put_complete = None
|
||||||
|
self._monref = None # holder of data returned from create_subscription
|
||||||
|
self._monref_mask = None
|
||||||
|
self._conn_started = False
|
||||||
|
if isinstance(callback, (tuple, list)):
|
||||||
|
for i, thiscb in enumerate(callback):
|
||||||
|
if callable(thiscb):
|
||||||
|
self.callbacks[i] = (thiscb, {})
|
||||||
|
elif callable(callback):
|
||||||
|
self.callbacks[0] = (callback, {})
|
||||||
|
|
||||||
|
self.chid = None
|
||||||
|
self.context = mock.MagicMock()
|
||||||
|
self._cache_key = (pvname, form, self.context)
|
||||||
|
self._reference_count = 0
|
||||||
|
for conn_cb in self.connection_callbacks:
|
||||||
|
conn_cb(pvname=pvname, conn=True, pv=self)
|
||||||
|
for acc_cb in self.access_callbacks:
|
||||||
|
acc_cb(True, True, pv=self)
|
||||||
|
|
||||||
|
def wait_for_connection(self, timeout=None):
|
||||||
|
return self.connected
|
||||||
|
|
||||||
|
def get_all_metadata_blocking(self, timeout):
|
||||||
|
md = self._args.copy()
|
||||||
|
md.pop("value", None)
|
||||||
|
return md
|
||||||
|
|
||||||
|
def get_all_metadata_callback(self, callback, *, timeout):
|
||||||
|
def get_metadata_thread(pvname):
|
||||||
|
md = self.get_all_metadata_blocking(timeout=timeout)
|
||||||
|
callback(pvname, md)
|
||||||
|
|
||||||
|
get_metadata_thread(pvname=self.pvname)
|
||||||
|
|
||||||
|
def put(
|
||||||
|
self, value, wait=False, timeout=None, use_complete=False, callback=None, callback_data=None
|
||||||
|
):
|
||||||
|
self.mock_data = value
|
||||||
|
if callback is not None:
|
||||||
|
callback(None, None, None)
|
||||||
|
|
||||||
|
def get_with_metadata(
|
||||||
|
self,
|
||||||
|
count=None,
|
||||||
|
as_string=False,
|
||||||
|
as_numpy=True,
|
||||||
|
timeout=None,
|
||||||
|
with_ctrlvars=False,
|
||||||
|
form=None,
|
||||||
|
use_monitor=True,
|
||||||
|
as_namespace=False,
|
||||||
|
):
|
||||||
|
return {"value": self.mock_data}
|
||||||
|
|
||||||
|
def get(
|
||||||
|
self,
|
||||||
|
count=None,
|
||||||
|
as_string=False,
|
||||||
|
as_numpy=True,
|
||||||
|
timeout=None,
|
||||||
|
with_ctrlvars=False,
|
||||||
|
use_monitor=True,
|
||||||
|
):
|
||||||
|
data = self.get_with_metadata(
|
||||||
|
count=count,
|
||||||
|
as_string=as_string,
|
||||||
|
as_numpy=as_numpy,
|
||||||
|
timeout=timeout,
|
||||||
|
with_ctrlvars=with_ctrlvars,
|
||||||
|
use_monitor=use_monitor,
|
||||||
|
)
|
||||||
|
return data["value"] if data is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceMock:
|
||||||
|
"""Device Mock. Used for testing in combination with the DeviceManagerMock
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): name of the device
|
||||||
|
value (float, optional): initial value of the device. Defaults to 0.0.
|
||||||
|
Returns:
|
||||||
|
DeviceMock: DeviceMock object
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, value: float = 0.0):
|
||||||
|
self.name = name
|
||||||
|
self.read_buffer = value
|
||||||
|
self._config = {"deviceConfig": {"limits": [-50, 50]}, "userParameter": None}
|
||||||
|
self._enabled_set = True
|
||||||
|
self._enabled = True
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
return {self.name: {"value": self.read_buffer}}
|
||||||
|
|
||||||
|
def readback(self):
|
||||||
|
return self.read_buffer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled_set(self) -> bool:
|
||||||
|
return self._enabled_set
|
||||||
|
|
||||||
|
@enabled_set.setter
|
||||||
|
def enabled_set(self, val: bool):
|
||||||
|
self._enabled_set = val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self) -> bool:
|
||||||
|
return self._enabled
|
||||||
|
|
||||||
|
@enabled.setter
|
||||||
|
def enabled(self, val: bool):
|
||||||
|
self._enabled = val
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_parameter(self):
|
||||||
|
return self._config["userParameter"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def obj(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class DMMock:
|
||||||
|
"""Mock for DeviceManager
|
||||||
|
|
||||||
|
The mocked DeviceManager creates a device containert and a producer.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.devices = DeviceContainer()
|
||||||
|
self.producer = ProducerMock()
|
||||||
|
|
||||||
|
def add_device(self, name: str, value: float = 0.0):
|
||||||
|
self.devices[name] = DeviceMock(name, value)
|
||||||
|
|
||||||
|
|
||||||
|
# #TODO check what is the difference to SynSignal!
|
||||||
|
# class MockSignal(Signal):
|
||||||
|
# """Can mock an OphydSignal"""
|
||||||
|
# def __init__(self, read_pv, *, string=False, name=None, parent=None, **kwargs):
|
||||||
|
# self.read_pv = read_pv
|
||||||
|
# self._string = bool(string)
|
||||||
|
# super().__init__(name=name, parent=parent, **kwargs)
|
||||||
|
# self._waited_for_connection = False
|
||||||
|
# self._subscriptions = []
|
||||||
|
|
||||||
|
# def wait_for_connection(self):
|
||||||
|
# self._waited_for_connection = True
|
||||||
|
|
||||||
|
# def subscribe(self, method, event_type, **kw):
|
||||||
|
# self._subscriptions.append((method, event_type, kw))
|
||||||
|
|
||||||
|
# def describe_configuration(self):
|
||||||
|
# return {self.name + "_conf": {"source": "SIM:test"}}
|
||||||
|
|
||||||
|
# def read_configuration(self):
|
||||||
|
# return {self.name + "_conf": {"value": 0}}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user