mirror of
https://github.com/bec-project/ophyd_devices.git
synced 2025-06-07 04:10:39 +02:00
feat(psi_device_base): add psi_device_base
This commit is contained in:
parent
5ce67e62cb
commit
ac4f0c5af7
176
ophyd_devices/interfaces/base_classes/psi_device_base.py
Normal file
176
ophyd_devices/interfaces/base_classes/psi_device_base.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
"""
|
||||||
|
Consider using the bec_device_base name for the base class.
|
||||||
|
I will use this name instead here to simplify comparisons between the two approaches
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ophyd import Device, DeviceStatus, Staged, StatusBase
|
||||||
|
|
||||||
|
from ophyd_devices.tests.utils import get_mock_scan_info
|
||||||
|
from ophyd_devices.utils.psi_device_base_utils import FileHandler, TaskHandler
|
||||||
|
|
||||||
|
|
||||||
|
class PSIDeviceBase(Device):
|
||||||
|
"""
|
||||||
|
Base class for all PSI ophyd devices to ensure consistent configuration
|
||||||
|
and communication with BEC services.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# These are all possible subscription types that the device_manager supports
|
||||||
|
# and automatically subscribes to
|
||||||
|
SUB_READBACK = "readback"
|
||||||
|
SUB_VALUE = "value"
|
||||||
|
SUB_DONE_MOVING = "done_moving"
|
||||||
|
SUB_MOTOR_IS_MOVING = "motor_is_moving"
|
||||||
|
SUB_PROGRESS = "progress"
|
||||||
|
SUB_FILE_EVENT = "file_event"
|
||||||
|
SUB_DEVICE_MONITOR_1D = "device_monitor_1d"
|
||||||
|
SUB_DEVICE_MONITOR_2D = "device_monitor_2d"
|
||||||
|
_default_sub = SUB_VALUE
|
||||||
|
|
||||||
|
def __init__(self, name: str, scan_info=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize the PSI Device Base class.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str) : Name of the device
|
||||||
|
scan_info (ScanInfo): The scan info to use.
|
||||||
|
"""
|
||||||
|
super().__init__(name=name, **kwargs)
|
||||||
|
self._stopped = False
|
||||||
|
self.task_handler = TaskHandler(parent=self)
|
||||||
|
if scan_info is None:
|
||||||
|
scan_info = get_mock_scan_info()
|
||||||
|
self.scan_info = scan_info
|
||||||
|
self.file_utils = FileHandler()
|
||||||
|
self.on_init()
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# Additional Properties and Attributes #
|
||||||
|
########################################
|
||||||
|
|
||||||
|
@property
|
||||||
|
def destroyed(self) -> bool:
|
||||||
|
"""Check if the device has been destroyed."""
|
||||||
|
return self._destroyed
|
||||||
|
|
||||||
|
@property
|
||||||
|
def staged(self) -> Staged:
|
||||||
|
"""Check if the device has been staged."""
|
||||||
|
return self._staged
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stopped(self) -> bool:
|
||||||
|
"""Check if the device has been stopped."""
|
||||||
|
return self._stopped
|
||||||
|
|
||||||
|
@stopped.setter
|
||||||
|
def stopped(self, value: bool):
|
||||||
|
self._stopped = value
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# Wrapper around Device class methods #
|
||||||
|
########################################
|
||||||
|
|
||||||
|
def stage(self) -> list[object] | StatusBase:
|
||||||
|
"""Stage the device."""
|
||||||
|
if self.staged != Staged.no:
|
||||||
|
return super().stage()
|
||||||
|
self.stopped = False
|
||||||
|
super_staged = super().stage()
|
||||||
|
status = self.on_stage() # pylint: disable=assignment-from-no-return
|
||||||
|
if isinstance(status, StatusBase):
|
||||||
|
return status
|
||||||
|
return super_staged
|
||||||
|
|
||||||
|
def unstage(self) -> list[object] | StatusBase:
|
||||||
|
"""Unstage the device."""
|
||||||
|
super_unstage = super().unstage()
|
||||||
|
status = self.on_unstage() # pylint: disable=assignment-from-no-return
|
||||||
|
if isinstance(status, StatusBase):
|
||||||
|
return status
|
||||||
|
return super_unstage
|
||||||
|
|
||||||
|
def pre_scan(self) -> StatusBase | None:
|
||||||
|
"""Pre-scan function."""
|
||||||
|
status = self.on_pre_scan() # pylint: disable=assignment-from-no-return
|
||||||
|
return status
|
||||||
|
|
||||||
|
def trigger(self) -> DeviceStatus:
|
||||||
|
"""Trigger the device."""
|
||||||
|
super_trigger = super().trigger()
|
||||||
|
status = self.on_trigger() # pylint: disable=assignment-from-no-return
|
||||||
|
return status if status else super_trigger
|
||||||
|
|
||||||
|
def complete(self) -> DeviceStatus:
|
||||||
|
"""Complete the device."""
|
||||||
|
status = self.on_complete() # pylint: disable=assignment-from-no-return
|
||||||
|
if isinstance(status, StatusBase):
|
||||||
|
return status
|
||||||
|
status = DeviceStatus(self)
|
||||||
|
status.set_finished()
|
||||||
|
return status
|
||||||
|
|
||||||
|
def kickoff(self) -> DeviceStatus:
|
||||||
|
"""Kickoff the device."""
|
||||||
|
status = self.on_kickoff() # pylint: disable=assignment-from-no-return
|
||||||
|
if isinstance(status, StatusBase):
|
||||||
|
return status
|
||||||
|
status = DeviceStatus(self)
|
||||||
|
status.set_finished()
|
||||||
|
return status
|
||||||
|
|
||||||
|
# pylint: disable=arguments-differ
|
||||||
|
def stop(self, success: bool = False) -> None:
|
||||||
|
"""Stop the device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
success (bool): True if the device was stopped successfully.
|
||||||
|
"""
|
||||||
|
self.on_stop()
|
||||||
|
super().stop(success=success)
|
||||||
|
self.stopped = True
|
||||||
|
|
||||||
|
########################################
|
||||||
|
# Beamline Specific Implementations #
|
||||||
|
########################################
|
||||||
|
|
||||||
|
def on_init(self) -> None:
|
||||||
|
"""
|
||||||
|
Called when the device is initialized.
|
||||||
|
|
||||||
|
No siganls are connected at this point,
|
||||||
|
thus should not be set here but in on_connected instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_connected(self) -> None:
|
||||||
|
"""
|
||||||
|
Called after the device is connected and its signals are connected.
|
||||||
|
Default values for signals should be set here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_stage(self) -> DeviceStatus | None:
|
||||||
|
"""
|
||||||
|
Called while staging the device.
|
||||||
|
|
||||||
|
Information about the upcoming scan can be accessed from the scan_info object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_unstage(self) -> DeviceStatus | None:
|
||||||
|
"""Called while unstaging the device."""
|
||||||
|
|
||||||
|
def on_pre_scan(self) -> DeviceStatus | None:
|
||||||
|
"""Called right before the scan starts on all devices automatically."""
|
||||||
|
|
||||||
|
def on_trigger(self) -> DeviceStatus | None:
|
||||||
|
"""Called when the device is triggered."""
|
||||||
|
|
||||||
|
def on_complete(self) -> DeviceStatus | None:
|
||||||
|
"""Called to inquire if a device has completed a scans."""
|
||||||
|
|
||||||
|
def on_kickoff(self) -> DeviceStatus | None:
|
||||||
|
"""Called to kickoff a device for a fly scan. Has to be called explicitly."""
|
||||||
|
|
||||||
|
def on_stop(self) -> None:
|
||||||
|
"""Called when the device is stopped."""
|
@ -1,7 +1,9 @@
|
|||||||
""" This module provides a range of protocols that describe the expected interface for different types of devices.
|
""" This module provides a range of protocols that describe the expected
|
||||||
|
interface for different types of devices.
|
||||||
|
|
||||||
The protocols below can be used as teamplates for functionality to be implemeted by different type of devices.
|
The protocols below can be used as teamplates for functionality to be implemeted
|
||||||
They further facilitate runtime checks on devices and provide a minimum set of properties required for a device to be loadable by BEC.
|
by different type of devices. They further facilitate runtime checks on devices
|
||||||
|
and provide a minimum set of properties required for a device to be loadable by BEC.
|
||||||
|
|
||||||
The protocols are:
|
The protocols are:
|
||||||
- BECBaseProtocol: Protocol for devices in BEC. All devices must at least implement this protocol.
|
- BECBaseProtocol: Protocol for devices in BEC. All devices must at least implement this protocol.
|
||||||
@ -11,17 +13,15 @@ The protocols are:
|
|||||||
- BECPositionerProtocol: Protocol for positioners.
|
- BECPositionerProtocol: Protocol for positioners.
|
||||||
- BECFlyerProtocol: Protocol with for flyers.
|
- BECFlyerProtocol: Protocol with for flyers.
|
||||||
|
|
||||||
Keep in mind, that a device of type flyer should generally also implement the BECDeviceProtocol that provides the required functionality for scans.
|
Keep in mind, that a device of type flyer should generally also implement the BECDeviceProtocol
|
||||||
Flyers in addition, also implement the BECFlyerProtocol. Similarly, positioners should also implement the BECDeviceProtocol and BECPositionerProtocol.
|
with the functionality needed for scans. In addition, flyers also implement the BECFlyerProtocol.
|
||||||
|
Similarly, positioners should also implement the BECDeviceProtocol and BECPositionerProtocol.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Protocol, runtime_checkable
|
from typing import Protocol, runtime_checkable
|
||||||
|
|
||||||
from bec_lib.file_utils import FileWriter
|
from ophyd import DeviceStatus, Kind, Staged
|
||||||
from ophyd import Component, DeviceStatus, Kind, Staged
|
|
||||||
|
|
||||||
from ophyd_devices.utils import bec_scaninfo_mixin
|
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
@ -349,8 +349,8 @@ class BECPositionerProtocol(BECDeviceProtocol, Protocol):
|
|||||||
|
|
||||||
def move(self, position: float) -> DeviceStatus:
|
def move(self, position: float) -> DeviceStatus:
|
||||||
"""Move method for positioners.
|
"""Move method for positioners.
|
||||||
The returned DeviceStatus is marked as done once the positioner has reached the target position.
|
The returned DeviceStatus is marked as done once the positioner has reached the target
|
||||||
DeviceStatus.wait() can be used to block until the move is completed.
|
position. DeviceStatus.wait() can be used to block until the move is completed.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
position: position to move to
|
position: position to move to
|
||||||
|
@ -1,161 +1,23 @@
|
|||||||
import traceback
|
""" Simulated 2D camera device"""
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from ophyd import Component as Cpt
|
from ophyd import Component as Cpt
|
||||||
from ophyd import DeviceStatus, Kind
|
from ophyd import Device, Kind, StatusBase
|
||||||
|
|
||||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||||
CustomDetectorMixin,
|
|
||||||
PSIDetectorBase,
|
|
||||||
)
|
|
||||||
from ophyd_devices.sim.sim_data import SimulatedDataCamera
|
from ophyd_devices.sim.sim_data import SimulatedDataCamera
|
||||||
from ophyd_devices.sim.sim_signals import ReadOnlySignal, SetableSignal
|
from ophyd_devices.sim.sim_signals import ReadOnlySignal, SetableSignal
|
||||||
from ophyd_devices.sim.sim_utils import H5Writer
|
from ophyd_devices.sim.sim_utils import H5Writer
|
||||||
from ophyd_devices.utils.errors import DeviceStopError
|
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
class SimCameraSetup(CustomDetectorMixin):
|
class SimCameraControl(Device):
|
||||||
"""Mixin class for the SimCamera device."""
|
"""SimCamera Control layer"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self._thread_trigger = None
|
|
||||||
self._thread_complete = None
|
|
||||||
self.file_path = None
|
|
||||||
|
|
||||||
def on_trigger(self) -> None:
|
|
||||||
"""Trigger the camera to acquire images.
|
|
||||||
|
|
||||||
This method can be called from BEC during a scan. It will acquire images and send them to BEC.
|
|
||||||
Whether the trigger is send from BEC is determined by the softwareTrigger argument in the device config.
|
|
||||||
|
|
||||||
Here, we also run a callback on SUB_MONITOR to send the image data the device_monitor endpoint in BEC.
|
|
||||||
"""
|
|
||||||
status = DeviceStatus(self.parent)
|
|
||||||
|
|
||||||
def on_trigger_call(status: DeviceStatus) -> None:
|
|
||||||
try:
|
|
||||||
for _ in range(self.parent.burst.get()):
|
|
||||||
data = self.parent.image.get()
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=data)
|
|
||||||
if self.parent.stopped:
|
|
||||||
raise DeviceStopError(f"{self.parent.name} was stopped")
|
|
||||||
if self.parent.write_to_disk.get():
|
|
||||||
self.parent.h5_writer.receive_data(data)
|
|
||||||
status.set_finished()
|
|
||||||
# pylint: disable=broad-except
|
|
||||||
except Exception as exc:
|
|
||||||
content = traceback.format_exc()
|
|
||||||
logger.warning(
|
|
||||||
f"Error in on_trigger_call in device {self.parent.name}. Error traceback: {content}"
|
|
||||||
)
|
|
||||||
status.set_exception(exc)
|
|
||||||
|
|
||||||
self._thread_trigger = Thread(target=on_trigger_call, args=(status,))
|
|
||||||
self._thread_trigger.start()
|
|
||||||
return status
|
|
||||||
|
|
||||||
def on_stage(self) -> None:
|
|
||||||
"""Stage the camera for upcoming scan
|
|
||||||
|
|
||||||
This method is called from BEC in preparation of a scan.
|
|
||||||
It receives metadata about the scan from BEC,
|
|
||||||
compiles it and prepares the camera for the scan.
|
|
||||||
|
|
||||||
FYI: No data is written to disk in the simulation, but upon each trigger it
|
|
||||||
is published to the device_monitor endpoint in REDIS.
|
|
||||||
"""
|
|
||||||
self.file_path = self.parent.filewriter.compile_full_filename(f"{self.parent.name}")
|
|
||||||
|
|
||||||
self.parent.frames.set(
|
|
||||||
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
|
|
||||||
)
|
|
||||||
self.parent.exp_time.set(self.parent.scaninfo.exp_time)
|
|
||||||
self.parent.burst.set(self.parent.scaninfo.frames_per_trigger)
|
|
||||||
if self.parent.write_to_disk.get():
|
|
||||||
self.parent.h5_writer.on_stage(file_path=self.file_path, h5_entry="/entry/data/data")
|
|
||||||
self.parent._run_subs(
|
|
||||||
sub_type=self.parent.SUB_FILE_EVENT,
|
|
||||||
file_path=self.file_path,
|
|
||||||
done=False,
|
|
||||||
successful=False,
|
|
||||||
hinted_location={"data": "/entry/data/data"},
|
|
||||||
)
|
|
||||||
self.parent.stopped = False
|
|
||||||
|
|
||||||
def on_complete(self) -> None:
|
|
||||||
"""Complete the motion of the simulated device."""
|
|
||||||
status = DeviceStatus(self.parent)
|
|
||||||
|
|
||||||
def on_complete_call(status: DeviceStatus) -> None:
|
|
||||||
try:
|
|
||||||
if self.parent.write_to_disk.get():
|
|
||||||
self.parent.h5_writer.on_complete()
|
|
||||||
self.parent._run_subs(
|
|
||||||
sub_type=self.parent.SUB_FILE_EVENT,
|
|
||||||
file_path=self.file_path,
|
|
||||||
done=True,
|
|
||||||
successful=True,
|
|
||||||
hinted_location={"data": "/entry/data/data"},
|
|
||||||
)
|
|
||||||
if self.parent.stopped:
|
|
||||||
raise DeviceStopError(f"{self.parent.name} was stopped")
|
|
||||||
status.set_finished()
|
|
||||||
# pylint: disable=broad-except
|
|
||||||
except Exception as exc:
|
|
||||||
content = traceback.format_exc()
|
|
||||||
logger.warning(
|
|
||||||
f"Error in on_complete call in device {self.parent.name}. Error traceback: {content}"
|
|
||||||
)
|
|
||||||
status.set_exception(exc)
|
|
||||||
|
|
||||||
self._thread_complete = Thread(target=on_complete_call, args=(status,), daemon=True)
|
|
||||||
self._thread_complete.start()
|
|
||||||
return status
|
|
||||||
|
|
||||||
def on_unstage(self):
|
|
||||||
"""Unstage the camera device."""
|
|
||||||
if self.parent.write_to_disk.get():
|
|
||||||
self.parent.h5_writer.on_unstage()
|
|
||||||
|
|
||||||
def on_stop(self) -> None:
|
|
||||||
"""Stop the camera acquisition."""
|
|
||||||
if self._thread_trigger:
|
|
||||||
self._thread_trigger.join()
|
|
||||||
if self._thread_complete:
|
|
||||||
self._thread_complete.join()
|
|
||||||
self.on_unstage()
|
|
||||||
self._thread_trigger = None
|
|
||||||
self._thread_complete = None
|
|
||||||
|
|
||||||
|
|
||||||
class SimCamera(PSIDetectorBase):
|
|
||||||
"""A simulated device mimic any 2D camera.
|
|
||||||
|
|
||||||
It's image is a computed signal, which is configurable by the user and from the command line.
|
|
||||||
The corresponding simulation class is sim_cls=SimulatedDataCamera, more details on defaults within the simulation class.
|
|
||||||
|
|
||||||
>>> camera = SimCamera(name="camera")
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
name (string) : Name of the device. This is the only required argmuent, passed on to all signals of the device.
|
|
||||||
precision (integer) : Precision of the readback in digits, written to .describe(). Default is 3 digits.
|
|
||||||
sim_init (dict) : Dictionary to initiate parameters of the simulation, check simulation type defaults for more details.
|
|
||||||
parent : Parent device, optional, is used internally if this signal/device is part of a larger device.
|
|
||||||
kind : A member the Kind IntEnum (or equivalent integer), optional. Default is Kind.normal. See Kind for options.
|
|
||||||
device_manager : DeviceManager from BEC, optional . Within startup of simulation, device_manager is passed on automatically.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
USER_ACCESS = ["sim", "registered_proxies"]
|
USER_ACCESS = ["sim", "registered_proxies"]
|
||||||
|
|
||||||
custom_prepare_cls = SimCameraSetup
|
|
||||||
sim_cls = SimulatedDataCamera
|
sim_cls = SimulatedDataCamera
|
||||||
SHAPE = (100, 100)
|
SHAPE = (100, 100)
|
||||||
BIT_DEPTH = np.uint16
|
BIT_DEPTH = np.uint16
|
||||||
@ -178,16 +40,13 @@ class SimCamera(PSIDetectorBase):
|
|||||||
)
|
)
|
||||||
write_to_disk = Cpt(SetableSignal, name="write_to_disk", value=False, kind=Kind.config)
|
write_to_disk = Cpt(SetableSignal, name="write_to_disk", value=False, kind=Kind.config)
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, name, *, parent=None, sim_init: dict = None, device_manager=None, **kwargs):
|
||||||
self, name, *, kind=None, parent=None, sim_init: dict = None, device_manager=None, **kwargs
|
|
||||||
):
|
|
||||||
self.sim_init = sim_init
|
self.sim_init = sim_init
|
||||||
|
self.device_manager = device_manager
|
||||||
self._registered_proxies = {}
|
self._registered_proxies = {}
|
||||||
self.sim = self.sim_cls(parent=self, **kwargs)
|
self.sim = self.sim_cls(parent=self, **kwargs)
|
||||||
self.h5_writer = H5Writer()
|
self.h5_writer = H5Writer()
|
||||||
super().__init__(
|
super().__init__(name=name, parent=parent, **kwargs)
|
||||||
name=name, parent=parent, kind=kind, device_manager=device_manager, **kwargs
|
|
||||||
)
|
|
||||||
if self.sim_init:
|
if self.sim_init:
|
||||||
self.sim.set_init(self.sim_init)
|
self.sim.set_init(self.sim_init)
|
||||||
|
|
||||||
@ -195,3 +54,104 @@ class SimCamera(PSIDetectorBase):
|
|||||||
def registered_proxies(self) -> None:
|
def registered_proxies(self) -> None:
|
||||||
"""Dictionary of registered signal_names and proxies."""
|
"""Dictionary of registered signal_names and proxies."""
|
||||||
return self._registered_proxies
|
return self._registered_proxies
|
||||||
|
|
||||||
|
|
||||||
|
class SimCamera(PSIDeviceBase, SimCameraControl):
|
||||||
|
"""A simulated device mimic any 2D camera.
|
||||||
|
|
||||||
|
It's image is a computed signal, which is configurable by the user and from the command line.
|
||||||
|
The corresponding simulation class is sim_cls=SimulatedDataCamera, more details on defaults within the simulation class.
|
||||||
|
|
||||||
|
>>> camera = SimCamera(name="camera")
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name (string) : Name of the device. This is the only required argmuent, passed on to all signals of the device.
|
||||||
|
precision (integer) : Precision of the readback in digits, written to .describe(). Default is 3 digits.
|
||||||
|
sim_init (dict) : Dictionary to initiate parameters of the simulation, check simulation type defaults for more details.
|
||||||
|
parent : Parent device, optional, is used internally if this signal/device is part of a larger device.
|
||||||
|
kind : A member the Kind IntEnum (or equivalent integer), optional. Default is Kind.normal. See Kind for options.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, scan_info=None, device_manager=None, **kwargs):
|
||||||
|
super().__init__(name=name, scan_info=scan_info, device_manager=device_manager, **kwargs)
|
||||||
|
self.file_path = None
|
||||||
|
|
||||||
|
def on_trigger(self) -> StatusBase:
|
||||||
|
"""Trigger the camera to acquire images.
|
||||||
|
|
||||||
|
This method can be called from BEC during a scan. It will acquire images and send them to BEC.
|
||||||
|
Whether the trigger is send from BEC is determined by the softwareTrigger argument in the device config.
|
||||||
|
|
||||||
|
Here, we also run a callback on SUB_MONITOR to send the image data the device_monitor endpoint in BEC.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def trigger_cam() -> None:
|
||||||
|
"""Trigger the camera to acquire images."""
|
||||||
|
for _ in range(self.burst.get()):
|
||||||
|
data = self.image.get()
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
self._run_subs(sub_type=self.SUB_MONITOR, value=data)
|
||||||
|
if self.write_to_disk.get():
|
||||||
|
self.h5_writer.receive_data(data)
|
||||||
|
|
||||||
|
status = self.task_handler.submit_task(trigger_cam)
|
||||||
|
return status
|
||||||
|
|
||||||
|
def on_stage(self) -> None:
|
||||||
|
"""Stage the camera for upcoming scan
|
||||||
|
|
||||||
|
This method is called from BEC in preparation of a scan.
|
||||||
|
It receives metadata about the scan from BEC,
|
||||||
|
compiles it and prepares the camera for the scan.
|
||||||
|
|
||||||
|
FYI: No data is written to disk in the simulation, but upon each trigger it
|
||||||
|
is published to the device_monitor endpoint in REDIS.
|
||||||
|
"""
|
||||||
|
self.file_path = self.file_utils.get_file_path(
|
||||||
|
scan_status_msg=self.scan_info.msg, name=self.name
|
||||||
|
)
|
||||||
|
self.frames.set(
|
||||||
|
self.scan_info.msg.num_points * self.scan_info.msg.scan_parameters["frames_per_trigger"]
|
||||||
|
)
|
||||||
|
self.exp_time.set(self.scan_info.msg.scan_parameters["exp_time"])
|
||||||
|
self.burst.set(self.scan_info.msg.scan_parameters["frames_per_trigger"])
|
||||||
|
if self.write_to_disk.get():
|
||||||
|
self.h5_writer.on_stage(file_path=self.file_path, h5_entry="/entry/data/data")
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
self._run_subs(
|
||||||
|
sub_type=self.SUB_FILE_EVENT,
|
||||||
|
file_path=self.file_path,
|
||||||
|
done=False,
|
||||||
|
successful=False,
|
||||||
|
hinted_location={"data": "/entry/data/data"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_complete(self) -> StatusBase:
|
||||||
|
"""Complete the motion of the simulated device."""
|
||||||
|
|
||||||
|
def complete_cam():
|
||||||
|
"""Complete the camera acquisition."""
|
||||||
|
if self.write_to_disk.get():
|
||||||
|
self.h5_writer.on_complete()
|
||||||
|
self._run_subs(
|
||||||
|
sub_type=self.SUB_FILE_EVENT,
|
||||||
|
file_path=self.file_path,
|
||||||
|
done=True,
|
||||||
|
successful=True,
|
||||||
|
hinted_location={"data": "/entry/data/data"},
|
||||||
|
)
|
||||||
|
|
||||||
|
status = self.task_handler.submit_task(complete_cam)
|
||||||
|
return status
|
||||||
|
|
||||||
|
def on_unstage(self) -> None:
|
||||||
|
"""Unstage the camera device."""
|
||||||
|
if self.write_to_disk.get():
|
||||||
|
self.h5_writer.on_unstage()
|
||||||
|
|
||||||
|
def on_stop(self) -> None:
|
||||||
|
"""Stop the camera acquisition."""
|
||||||
|
self.task_handler.shutdown()
|
||||||
|
self.on_unstage()
|
||||||
|
@ -106,7 +106,7 @@ class SimulatedDataBase(ABC):
|
|||||||
def execute_simulation_method(self, *args, method=None, signal_name: str = "", **kwargs) -> any:
|
def execute_simulation_method(self, *args, method=None, signal_name: str = "", **kwargs) -> any:
|
||||||
"""
|
"""
|
||||||
Execute either the provided method or reroutes the method execution
|
Execute either the provided method or reroutes the method execution
|
||||||
to a device proxy in case it is registered in self.parentregistered_proxies.
|
to a device proxy in case it is registered in self.parent.registered_proxies.
|
||||||
"""
|
"""
|
||||||
if self.registered_proxies and self.parent.device_manager:
|
if self.registered_proxies and self.parent.device_manager:
|
||||||
for proxy_name, signal in self.registered_proxies.items():
|
for proxy_name, signal in self.registered_proxies.items():
|
||||||
|
@ -1,22 +1,16 @@
|
|||||||
"""Module for simulated monitor devices."""
|
"""Module for simulated monitor devices."""
|
||||||
|
|
||||||
import traceback
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bec_lib import messages
|
from bec_lib import messages
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from ophyd import Component as Cpt
|
from ophyd import Component as Cpt
|
||||||
from ophyd import Device, DeviceStatus, Kind
|
from ophyd import Device, Kind, StatusBase
|
||||||
|
|
||||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||||
CustomDetectorMixin,
|
|
||||||
PSIDetectorBase,
|
|
||||||
)
|
|
||||||
from ophyd_devices.sim.sim_data import SimulatedDataMonitor
|
from ophyd_devices.sim.sim_data import SimulatedDataMonitor
|
||||||
from ophyd_devices.sim.sim_signals import ReadOnlySignal, SetableSignal
|
from ophyd_devices.sim.sim_signals import ReadOnlySignal, SetableSignal
|
||||||
from ophyd_devices.utils.errors import DeviceStopError
|
from ophyd_devices.utils import bec_utils
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@ -26,18 +20,25 @@ class SimMonitor(ReadOnlySignal):
|
|||||||
A simulated device mimic any 1D Axis (position, temperature, beam).
|
A simulated device mimic any 1D Axis (position, temperature, beam).
|
||||||
|
|
||||||
It's readback is a computed signal, which is configurable by the user and from the command line.
|
It's readback is a computed signal, which is configurable by the user and from the command line.
|
||||||
The corresponding simulation class is sim_cls=SimulatedDataMonitor, more details on defaults within the simulation class.
|
The corresponding simulation class is sim_cls=SimulatedDataMonitor, more details on defaults
|
||||||
|
within the simulation class.
|
||||||
|
|
||||||
>>> monitor = SimMonitor(name="monitor")
|
>>> monitor = SimMonitor(name="monitor")
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
name (string) : Name of the device. This is the only required argmuent, passed on to all signals of the device.
|
name (string) : Name of the device. This is the only required argmuent,
|
||||||
precision (integer) : Precision of the readback in digits, written to .describe(). Default is 3 digits.
|
passed on to all signals of the device.
|
||||||
sim_init (dict) : Dictionary to initiate parameters of the simulation, check simulation type defaults for more details.
|
precision (integer) : Precision of the readback in digits, written to .describe().
|
||||||
parent : Parent device, optional, is used internally if this signal/device is part of a larger device.
|
Default is 3 digits.
|
||||||
kind : A member the Kind IntEnum (or equivalent integer), optional. Default is Kind.normal. See Kind for options.
|
sim_init (dict) : Dictionary to initiate parameters of the simulation,
|
||||||
device_manager : DeviceManager from BEC, optional . Within startup of simulation, device_manager is passed on automatically.
|
check simulation type defaults for more details.
|
||||||
|
parent : Parent device, optional, is used internally if this
|
||||||
|
signal/device is part of a larger device.
|
||||||
|
kind : A member the Kind IntEnum (or equivalent integer), optional.
|
||||||
|
Default is Kind.normal. See Kind for options.
|
||||||
|
device_manager : DeviceManager from BEC, optional . Within startup of simulation,
|
||||||
|
device_manager is passed on automatically.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -81,135 +82,11 @@ class SimMonitor(ReadOnlySignal):
|
|||||||
return self._registered_proxies
|
return self._registered_proxies
|
||||||
|
|
||||||
|
|
||||||
class SimMonitorAsyncPrepare(CustomDetectorMixin):
|
class SimMonitorAsyncControl(Device):
|
||||||
"""Custom prepare for the SimMonitorAsync class."""
|
"""SimMonitor Sync Control Device"""
|
||||||
|
|
||||||
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
|
|
||||||
super().__init__(*args, parent=parent, **kwargs)
|
|
||||||
self._stream_ttl = 1800
|
|
||||||
self._random_send_interval = None
|
|
||||||
self._counter = 0
|
|
||||||
self._thread_trigger = None
|
|
||||||
self._thread_complete = None
|
|
||||||
self.prep_random_interval()
|
|
||||||
self.parent.current_trigger.subscribe(self._progress_update, run=False)
|
|
||||||
|
|
||||||
def clear_buffer(self):
|
|
||||||
"""Clear the data buffer."""
|
|
||||||
self.parent.data_buffer["value"].clear()
|
|
||||||
self.parent.data_buffer["timestamp"].clear()
|
|
||||||
|
|
||||||
def prep_random_interval(self):
|
|
||||||
"""Prepare counter and random interval to send data to BEC."""
|
|
||||||
self._random_send_interval = np.random.randint(1, 10)
|
|
||||||
self.parent.current_trigger.set(0).wait()
|
|
||||||
self._counter = self.parent.current_trigger.get()
|
|
||||||
|
|
||||||
def on_stage(self):
|
|
||||||
"""Prepare the device for staging."""
|
|
||||||
self.clear_buffer()
|
|
||||||
self.prep_random_interval()
|
|
||||||
|
|
||||||
def on_complete(self):
|
|
||||||
"""Prepare the device for completion."""
|
|
||||||
status = DeviceStatus(self.parent)
|
|
||||||
|
|
||||||
def on_complete_call(status: DeviceStatus) -> None:
|
|
||||||
try:
|
|
||||||
if self.parent.data_buffer["value"]:
|
|
||||||
self._send_data_to_bec()
|
|
||||||
if self.parent.stopped:
|
|
||||||
raise DeviceStopError(f"{self.parent.name} was stopped")
|
|
||||||
status.set_finished()
|
|
||||||
# pylint: disable=broad-except
|
|
||||||
except Exception as exc:
|
|
||||||
content = traceback.format_exc()
|
|
||||||
status.set_exception(exc=exc)
|
|
||||||
logger.warning(f"Error in {self.parent.name} on_complete; Traceback: {content}")
|
|
||||||
|
|
||||||
self._thread_complete = Thread(target=on_complete_call, args=(status,))
|
|
||||||
self._thread_complete.start()
|
|
||||||
return status
|
|
||||||
|
|
||||||
def _send_data_to_bec(self) -> None:
|
|
||||||
"""Sends bundled data to BEC"""
|
|
||||||
if self.parent.scaninfo.scan_msg is None:
|
|
||||||
return
|
|
||||||
metadata = self.parent.scaninfo.scan_msg.metadata
|
|
||||||
metadata.update({"async_update": self.parent.async_update.get()})
|
|
||||||
|
|
||||||
msg = messages.DeviceMessage(
|
|
||||||
signals={self.parent.readback.name: self.parent.data_buffer},
|
|
||||||
metadata=self.parent.scaninfo.scan_msg.metadata,
|
|
||||||
)
|
|
||||||
self.parent.connector.xadd(
|
|
||||||
MessageEndpoints.device_async_readback(
|
|
||||||
scan_id=self.parent.scaninfo.scan_id, device=self.parent.name
|
|
||||||
),
|
|
||||||
{"data": msg},
|
|
||||||
expire=self._stream_ttl,
|
|
||||||
)
|
|
||||||
self.clear_buffer()
|
|
||||||
|
|
||||||
def on_trigger(self):
|
|
||||||
"""Prepare the device for triggering."""
|
|
||||||
status = DeviceStatus(self.parent)
|
|
||||||
|
|
||||||
def on_trigger_call(status: DeviceStatus) -> None:
|
|
||||||
try:
|
|
||||||
self.parent.data_buffer["value"].append(self.parent.readback.get())
|
|
||||||
self.parent.data_buffer["timestamp"].append(self.parent.readback.timestamp)
|
|
||||||
self._counter += 1
|
|
||||||
self.parent.current_trigger.set(self._counter).wait()
|
|
||||||
if self._counter % self._random_send_interval == 0:
|
|
||||||
self._send_data_to_bec()
|
|
||||||
if self.parent.stopped:
|
|
||||||
raise DeviceStopError(f"{self.parent.name} was stopped")
|
|
||||||
status.set_finished()
|
|
||||||
# pylint: disable=broad-except
|
|
||||||
except Exception as exc:
|
|
||||||
content = traceback.format_exc()
|
|
||||||
logger.warning(
|
|
||||||
f"Error in on_trigger_call in device {self.parent.name}; Traceback: {content}"
|
|
||||||
)
|
|
||||||
status.set_exception(exc=exc)
|
|
||||||
|
|
||||||
self._thread_trigger = Thread(target=on_trigger_call, args=(status,))
|
|
||||||
self._thread_trigger.start()
|
|
||||||
return status
|
|
||||||
|
|
||||||
def _progress_update(self, value: int, **kwargs):
|
|
||||||
"""Update the progress of the device."""
|
|
||||||
max_value = self.parent.scaninfo.num_points
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
self.parent._run_subs(
|
|
||||||
sub_type=self.parent.SUB_PROGRESS,
|
|
||||||
value=value,
|
|
||||||
max_value=max_value,
|
|
||||||
done=bool(max_value == value),
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_stop(self):
|
|
||||||
"""Stop the device."""
|
|
||||||
if self._thread_trigger:
|
|
||||||
self._thread_trigger.join()
|
|
||||||
if self._thread_complete:
|
|
||||||
self._thread_complete.join()
|
|
||||||
self._thread_trigger = None
|
|
||||||
self._thread_complete = None
|
|
||||||
|
|
||||||
|
|
||||||
class SimMonitorAsync(PSIDetectorBase):
|
|
||||||
"""
|
|
||||||
A simulated device to mimic the behaviour of an asynchronous monitor.
|
|
||||||
|
|
||||||
During a scan, this device will send data not in sync with the point ID to BEC,
|
|
||||||
but buffer data and send it in random intervals.s
|
|
||||||
"""
|
|
||||||
|
|
||||||
USER_ACCESS = ["sim", "registered_proxies", "async_update"]
|
USER_ACCESS = ["sim", "registered_proxies", "async_update"]
|
||||||
|
|
||||||
custom_prepare_cls = SimMonitorAsyncPrepare
|
|
||||||
sim_cls = SimulatedDataMonitor
|
sim_cls = SimulatedDataMonitor
|
||||||
BIT_DEPTH = np.uint32
|
BIT_DEPTH = np.uint32
|
||||||
|
|
||||||
@ -221,17 +98,17 @@ class SimMonitorAsync(PSIDetectorBase):
|
|||||||
SUB_PROGRESS = "progress"
|
SUB_PROGRESS = "progress"
|
||||||
_default_sub = SUB_READBACK
|
_default_sub = SUB_READBACK
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, name, *, sim_init: dict = None, parent=None, device_manager=None, **kwargs):
|
||||||
self, name, *, sim_init: dict = None, parent=None, kind=None, device_manager=None, **kwargs
|
if device_manager:
|
||||||
):
|
self.device_manager = device_manager
|
||||||
|
else:
|
||||||
|
self.device_manager = bec_utils.DMMock()
|
||||||
|
self.connector = self.device_manager.connector
|
||||||
self.sim_init = sim_init
|
self.sim_init = sim_init
|
||||||
self.device_manager = device_manager
|
|
||||||
self.sim = self.sim_cls(parent=self, **kwargs)
|
self.sim = self.sim_cls(parent=self, **kwargs)
|
||||||
self._registered_proxies = {}
|
self._registered_proxies = {}
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(name=name, parent=parent, **kwargs)
|
||||||
name=name, parent=parent, kind=kind, device_manager=device_manager, **kwargs
|
|
||||||
)
|
|
||||||
self.sim.sim_state[self.name] = self.sim.sim_state.pop(self.readback.name, None)
|
self.sim.sim_state[self.name] = self.sim.sim_state.pop(self.readback.name, None)
|
||||||
self.readback.name = self.name
|
self.readback.name = self.name
|
||||||
self._data_buffer = {"value": [], "timestamp": []}
|
self._data_buffer = {"value": [], "timestamp": []}
|
||||||
@ -247,3 +124,101 @@ class SimMonitorAsync(PSIDetectorBase):
|
|||||||
def registered_proxies(self) -> None:
|
def registered_proxies(self) -> None:
|
||||||
"""Dictionary of registered signal_names and proxies."""
|
"""Dictionary of registered signal_names and proxies."""
|
||||||
return self._registered_proxies
|
return self._registered_proxies
|
||||||
|
|
||||||
|
|
||||||
|
class SimMonitorAsync(PSIDeviceBase, SimMonitorAsyncControl):
|
||||||
|
"""
|
||||||
|
A simulated device to mimic the behaviour of an asynchronous monitor.
|
||||||
|
|
||||||
|
During a scan, this device will send data not in sync with the point ID to BEC,
|
||||||
|
but buffer data and send it in random intervals.s
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, name: str, scan_info=None, parent: Device = None, device_manager=None, **kwargs
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
name=name, scan_info=scan_info, parent=parent, device_manager=device_manager, **kwargs
|
||||||
|
)
|
||||||
|
self._stream_ttl = 1800
|
||||||
|
self._random_send_interval = None
|
||||||
|
self._counter = 0
|
||||||
|
self.prep_random_interval()
|
||||||
|
|
||||||
|
def on_connected(self):
|
||||||
|
self.current_trigger.subscribe(self._progress_update, run=False)
|
||||||
|
|
||||||
|
def clear_buffer(self):
|
||||||
|
"""Clear the data buffer."""
|
||||||
|
self.data_buffer["value"].clear()
|
||||||
|
self.data_buffer["timestamp"].clear()
|
||||||
|
|
||||||
|
def prep_random_interval(self):
|
||||||
|
"""Prepare counter and random interval to send data to BEC."""
|
||||||
|
self._random_send_interval = np.random.randint(1, 10)
|
||||||
|
self.current_trigger.set(0).wait()
|
||||||
|
self._counter = self.current_trigger.get()
|
||||||
|
|
||||||
|
def on_stage(self):
|
||||||
|
"""Prepare the device for staging."""
|
||||||
|
self.clear_buffer()
|
||||||
|
self.prep_random_interval()
|
||||||
|
|
||||||
|
def on_complete(self) -> StatusBase:
|
||||||
|
"""Prepare the device for completion."""
|
||||||
|
|
||||||
|
def complete_action():
|
||||||
|
if self.data_buffer["value"]:
|
||||||
|
self._send_data_to_bec()
|
||||||
|
|
||||||
|
status = self.task_handler.submit_task(complete_action)
|
||||||
|
return status
|
||||||
|
|
||||||
|
def _send_data_to_bec(self) -> None:
|
||||||
|
"""Sends bundled data to BEC"""
|
||||||
|
if self.scan_info.msg is None:
|
||||||
|
return
|
||||||
|
metadata = self.scan_info.msg.metadata
|
||||||
|
metadata.update({"async_update": self.async_update.get()})
|
||||||
|
|
||||||
|
msg = messages.DeviceMessage(
|
||||||
|
signals={self.readback.name: self.data_buffer}, metadata=self.scan_info.msg.metadata
|
||||||
|
)
|
||||||
|
self.connector.xadd(
|
||||||
|
MessageEndpoints.device_async_readback(
|
||||||
|
scan_id=self.scan_info.msg.scan_id, device=self.name
|
||||||
|
),
|
||||||
|
{"data": msg},
|
||||||
|
expire=self._stream_ttl,
|
||||||
|
)
|
||||||
|
self.clear_buffer()
|
||||||
|
|
||||||
|
def on_trigger(self):
|
||||||
|
"""Prepare the device for triggering."""
|
||||||
|
|
||||||
|
def trigger_action():
|
||||||
|
"""Trigger actions"""
|
||||||
|
self.data_buffer["value"].append(self.readback.get())
|
||||||
|
self.data_buffer["timestamp"].append(self.readback.timestamp)
|
||||||
|
self._counter += 1
|
||||||
|
self.current_trigger.set(self._counter).wait()
|
||||||
|
if self._counter % self._random_send_interval == 0:
|
||||||
|
self._send_data_to_bec()
|
||||||
|
|
||||||
|
status = self.task_handler.submit_task(trigger_action)
|
||||||
|
return status
|
||||||
|
|
||||||
|
def _progress_update(self, value: int, **kwargs):
|
||||||
|
"""Update the progress of the device."""
|
||||||
|
max_value = self.scan_info.msg.num_points
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
self._run_subs(
|
||||||
|
sub_type=self.SUB_PROGRESS,
|
||||||
|
value=value,
|
||||||
|
max_value=max_value,
|
||||||
|
done=bool(max_value == value),
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_stop(self):
|
||||||
|
"""Stop the device."""
|
||||||
|
self.task_handler.shutdown()
|
||||||
|
@ -15,7 +15,6 @@ from ophyd import Device, DeviceStatus, Kind, Staged
|
|||||||
from ophyd_devices.sim.sim_data import SimulatedDataWaveform
|
from ophyd_devices.sim.sim_data import SimulatedDataWaveform
|
||||||
from ophyd_devices.sim.sim_signals import ReadOnlySignal, SetableSignal
|
from ophyd_devices.sim.sim_signals import ReadOnlySignal, SetableSignal
|
||||||
from ophyd_devices.utils import bec_utils
|
from ophyd_devices.utils import bec_utils
|
||||||
from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin
|
|
||||||
from ophyd_devices.utils.errors import DeviceStopError
|
from ophyd_devices.utils.errors import DeviceStopError
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
@ -67,7 +66,15 @@ class SimWaveform(Device):
|
|||||||
async_update = Cpt(SetableSignal, value="append", kind=Kind.config)
|
async_update = Cpt(SetableSignal, value="append", kind=Kind.config)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name, *, kind=None, parent=None, sim_init: dict = None, device_manager=None, **kwargs
|
self,
|
||||||
|
name,
|
||||||
|
*,
|
||||||
|
kind=None,
|
||||||
|
parent=None,
|
||||||
|
sim_init: dict = None,
|
||||||
|
device_manager=None,
|
||||||
|
scan_info=None,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
self.sim_init = sim_init
|
self.sim_init = sim_init
|
||||||
self._registered_proxies = {}
|
self._registered_proxies = {}
|
||||||
@ -83,9 +90,8 @@ class SimWaveform(Device):
|
|||||||
self._stream_ttl = 1800 # 30 min max
|
self._stream_ttl = 1800 # 30 min max
|
||||||
self.stopped = False
|
self.stopped = False
|
||||||
self._staged = Staged.no
|
self._staged = Staged.no
|
||||||
self.scaninfo = None
|
|
||||||
self._trigger_thread = None
|
self._trigger_thread = None
|
||||||
self._update_scaninfo()
|
self.scan_info = scan_info
|
||||||
if self.sim_init:
|
if self.sim_init:
|
||||||
self.sim.set_init(self.sim_init)
|
self.sim.set_init(self.sim_init)
|
||||||
|
|
||||||
@ -124,7 +130,7 @@ class SimWaveform(Device):
|
|||||||
|
|
||||||
def _send_async_update(self):
|
def _send_async_update(self):
|
||||||
"""Send the async update to BEC."""
|
"""Send the async update to BEC."""
|
||||||
metadata = self.scaninfo.scan_msg.metadata
|
metadata = self.scan_info.msg.metadata
|
||||||
async_update_type = self.async_update.get()
|
async_update_type = self.async_update.get()
|
||||||
if async_update_type not in ["extend", "append"]:
|
if async_update_type not in ["extend", "append"]:
|
||||||
raise ValueError(f"Invalid async_update type: {async_update_type}")
|
raise ValueError(f"Invalid async_update type: {async_update_type}")
|
||||||
@ -134,19 +140,15 @@ class SimWaveform(Device):
|
|||||||
signals={self.waveform.name: {"value": self.waveform.get(), "timestamp": time.time()}},
|
signals={self.waveform.name: {"value": self.waveform.get(), "timestamp": time.time()}},
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
)
|
)
|
||||||
# logger.warning(f"Adding async update to {self.name} and {self.scaninfo.scan_id}")
|
# logger.warning(f"Adding async update to {self.name} and {self.scan_info.msg.scan_id}")
|
||||||
self.connector.xadd(
|
self.connector.xadd(
|
||||||
MessageEndpoints.device_async_readback(scan_id=self.scaninfo.scan_id, device=self.name),
|
MessageEndpoints.device_async_readback(
|
||||||
|
scan_id=self.scan_info.msg.scan_id, device=self.name
|
||||||
|
),
|
||||||
{"data": msg},
|
{"data": msg},
|
||||||
expire=self._stream_ttl,
|
expire=self._stream_ttl,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _update_scaninfo(self) -> None:
|
|
||||||
"""Update scaninfo from BecScaninfoMixing
|
|
||||||
This depends on device manager and operation/sim_mode
|
|
||||||
"""
|
|
||||||
self.scaninfo = BecScaninfoMixin(self.device_manager)
|
|
||||||
|
|
||||||
def stage(self) -> list[object]:
|
def stage(self) -> list[object]:
|
||||||
"""Stage the camera for upcoming scan
|
"""Stage the camera for upcoming scan
|
||||||
|
|
||||||
@ -160,17 +162,18 @@ class SimWaveform(Device):
|
|||||||
if self._staged is Staged.yes:
|
if self._staged is Staged.yes:
|
||||||
|
|
||||||
return super().stage()
|
return super().stage()
|
||||||
self.scaninfo.load_scan_metadata()
|
|
||||||
self.file_path.set(
|
self.file_path.set(
|
||||||
os.path.join(
|
os.path.join(
|
||||||
self.file_path.get(), self.file_pattern.get().format(self.scaninfo.scan_number)
|
self.file_path.get(), self.file_pattern.get().format(self.scan_info.msg.scan_number)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.frames.set(self.scaninfo.num_points * self.scaninfo.frames_per_trigger)
|
self.frames.set(
|
||||||
self.exp_time.set(self.scaninfo.exp_time)
|
self.scan_info.msg.num_points * self.scan_info.msg.scan_parameters["frames_per_trigger"]
|
||||||
self.burst.set(self.scaninfo.frames_per_trigger)
|
)
|
||||||
|
self.exp_time.set(self.scan_info.msg.scan_parameters["exp_time"])
|
||||||
|
self.burst.set(self.scan_info.msg.scan_parameters["frames_per_trigger"])
|
||||||
self.stopped = False
|
self.stopped = False
|
||||||
logger.warning(f"Staged {self.name}, scan_id : {self.scaninfo.scan_id}")
|
logger.warning(f"Staged {self.name}, scan_id : {self.scan_info.msg.scan_id}")
|
||||||
return super().stage()
|
return super().stage()
|
||||||
|
|
||||||
def unstage(self) -> list[object]:
|
def unstage(self) -> list[object]:
|
||||||
|
@ -1,5 +1,21 @@
|
|||||||
|
""" Utilities to mock and test devices."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
from bec_lib.devicemanager import ScanInfo
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from bec_lib.utils.import_utils import lazy_import_from
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bec_lib.messages import ScanStatusMessage
|
||||||
|
else:
|
||||||
|
# TODO: put back normal import when Pydantic gets faster
|
||||||
|
ScanStatusMessage = lazy_import_from("bec_lib.messages", ("ScanStatusMessage",))
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
def patch_dual_pvs(device):
|
def patch_dual_pvs(device):
|
||||||
"""Patch dual PVs"""
|
"""Patch dual PVs"""
|
||||||
@ -255,3 +271,128 @@ class MockPV:
|
|||||||
use_monitor=use_monitor,
|
use_monitor=use_monitor,
|
||||||
)
|
)
|
||||||
return data["value"] if data is not None else None
|
return data["value"] if data is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_mock_scan_info():
|
||||||
|
"""
|
||||||
|
Get a mock scan info object.
|
||||||
|
"""
|
||||||
|
return ScanInfo(msg=fake_scan_status_msg())
|
||||||
|
|
||||||
|
|
||||||
|
def fake_scan_status_msg():
|
||||||
|
"""
|
||||||
|
Create a fake scan status message.
|
||||||
|
"""
|
||||||
|
logger.warning(
|
||||||
|
("Device is not connected to a Redis server. Fetching mocked ScanStatusMessage.")
|
||||||
|
)
|
||||||
|
return ScanStatusMessage(
|
||||||
|
metadata={},
|
||||||
|
scan_id="mock_scan_id",
|
||||||
|
status="closed",
|
||||||
|
scan_number=0,
|
||||||
|
session_id=None,
|
||||||
|
num_points=11,
|
||||||
|
scan_name="mock_line_scan",
|
||||||
|
scan_type="step",
|
||||||
|
dataset_number=0,
|
||||||
|
scan_report_devices=["samx"],
|
||||||
|
user_metadata={},
|
||||||
|
readout_priority={
|
||||||
|
"monitored": ["bpm4a", "samx"],
|
||||||
|
"baseline": ["eyex"],
|
||||||
|
"async": ["waveform"],
|
||||||
|
"continuous": [],
|
||||||
|
"on_request": ["flyer_sim"],
|
||||||
|
},
|
||||||
|
scan_parameters={
|
||||||
|
"exp_time": 0,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
"settling_time": 0,
|
||||||
|
"readout_time": 0,
|
||||||
|
"optim_trajectory": None,
|
||||||
|
"return_to_start": True,
|
||||||
|
"relative": True,
|
||||||
|
"system_config": {"file_suffix": None, "file_directory": None},
|
||||||
|
},
|
||||||
|
request_inputs={
|
||||||
|
"arg_bundle": ["samx", -10, 10],
|
||||||
|
"inputs": {},
|
||||||
|
"kwargs": {
|
||||||
|
"steps": 11,
|
||||||
|
"relative": True,
|
||||||
|
"system_config": {"file_suffix": None, "file_directory": None},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
info={
|
||||||
|
"readout_priority": {
|
||||||
|
"monitored": ["bpm4a", "samx"],
|
||||||
|
"baseline": ["eyex"],
|
||||||
|
"async": ["waveform"],
|
||||||
|
"continuous": [],
|
||||||
|
"on_request": ["flyer_sim"],
|
||||||
|
},
|
||||||
|
"file_suffix": None,
|
||||||
|
"file_directory": None,
|
||||||
|
"user_metadata": {},
|
||||||
|
"RID": "a1d86f61-191c-4460-bcd6-f33c61b395ea",
|
||||||
|
"scan_id": "3edb8219-75a7-4791-8f86-d5ca112b771a",
|
||||||
|
"queue_id": "0f3639ee-899f-4ad1-9e71-f40514c937ef",
|
||||||
|
"scan_motors": ["samx"],
|
||||||
|
"num_points": 11,
|
||||||
|
"positions": [
|
||||||
|
[-10.0],
|
||||||
|
[-8.0],
|
||||||
|
[-6.0],
|
||||||
|
[-4.0],
|
||||||
|
[-2.0],
|
||||||
|
[0.0],
|
||||||
|
[2.0],
|
||||||
|
[4.0],
|
||||||
|
[6.0],
|
||||||
|
[8.0],
|
||||||
|
[10.0],
|
||||||
|
],
|
||||||
|
"file_path": "./data/test_file",
|
||||||
|
"scan_name": "mock_line_scan",
|
||||||
|
"scan_type": "step",
|
||||||
|
"scan_number": 0,
|
||||||
|
"dataset_number": 0,
|
||||||
|
"exp_time": 0,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
"settling_time": 0,
|
||||||
|
"readout_time": 0,
|
||||||
|
"scan_report_devices": ["samx"],
|
||||||
|
"monitor_sync": "bec",
|
||||||
|
"scan_parameters": {
|
||||||
|
"exp_time": 0,
|
||||||
|
"frames_per_trigger": 1,
|
||||||
|
"settling_time": 0,
|
||||||
|
"readout_time": 0,
|
||||||
|
"optim_trajectory": None,
|
||||||
|
"return_to_start": True,
|
||||||
|
"relative": True,
|
||||||
|
"system_config": {"file_suffix": None, "file_directory": None},
|
||||||
|
},
|
||||||
|
"request_inputs": {
|
||||||
|
"arg_bundle": ["samx", -10, 10],
|
||||||
|
"inputs": {},
|
||||||
|
"kwargs": {
|
||||||
|
"steps": 11,
|
||||||
|
"relative": True,
|
||||||
|
"system_config": {"file_suffix": None, "file_directory": None},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"scan_msgs": [
|
||||||
|
"metadata={'file_suffix': None, 'file_directory': None, 'user_metadata': {}, 'RID': 'a1d86f61-191c-4460-bcd6-f33c61b395ea'} scan_type='mock_line_scan' parameter={'args': {'samx': [-10, 10]}, 'kwargs': {'steps': 11, 'relative': True, 'system_config': {'file_suffix': None, 'file_directory': None}}} queue='primary'"
|
||||||
|
],
|
||||||
|
"args": {"samx": [-10, 10]},
|
||||||
|
"kwargs": {
|
||||||
|
"steps": 11,
|
||||||
|
"relative": True,
|
||||||
|
"system_config": {"file_suffix": None, "file_directory": None},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timestamp=1737100681.694211,
|
||||||
|
)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
""" Utility class linked to BEC"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from bec_lib import bec_logger
|
from bec_lib import bec_logger
|
||||||
@ -11,8 +13,9 @@ logger = bec_logger.logger
|
|||||||
DEFAULT_EPICSSIGNAL_VALUE = object()
|
DEFAULT_EPICSSIGNAL_VALUE = object()
|
||||||
|
|
||||||
|
|
||||||
# TODO maybe specify here that this DeviceMock is for usage in the DeviceServer
|
|
||||||
class DeviceMock:
|
class DeviceMock:
|
||||||
|
"""Mock for Device"""
|
||||||
|
|
||||||
def __init__(self, name: str, value: float = 0.0):
|
def __init__(self, name: str, value: float = 0.0):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.read_buffer = value
|
self.read_buffer = value
|
||||||
@ -21,13 +24,16 @@ class DeviceMock:
|
|||||||
self._enabled = True
|
self._enabled = True
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
|
"""Return the current value of the device"""
|
||||||
return {self.name: {"value": self.read_buffer}}
|
return {self.name: {"value": self.read_buffer}}
|
||||||
|
|
||||||
def readback(self):
|
def readback(self):
|
||||||
|
"""Return the current value of the device"""
|
||||||
return self.read_buffer
|
return self.read_buffer
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def read_only(self) -> bool:
|
def read_only(self) -> bool:
|
||||||
|
"""Get the read only status of the device"""
|
||||||
return self._read_only
|
return self._read_only
|
||||||
|
|
||||||
@read_only.setter
|
@read_only.setter
|
||||||
@ -36,6 +42,7 @@ class DeviceMock:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self) -> bool:
|
def enabled(self) -> bool:
|
||||||
|
"""Get the enabled status of the device"""
|
||||||
return self._enabled
|
return self._enabled
|
||||||
|
|
||||||
@enabled.setter
|
@enabled.setter
|
||||||
@ -44,10 +51,12 @@ class DeviceMock:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def user_parameter(self):
|
def user_parameter(self):
|
||||||
|
"""Get the user parameter of the device"""
|
||||||
return self._config["userParameter"]
|
return self._config["userParameter"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def obj(self):
|
def obj(self):
|
||||||
|
"""Get the device object"""
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
193
ophyd_devices/utils/psi_device_base_utils.py
Normal file
193
ophyd_devices/utils/psi_device_base_utils.py
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
""" Utility handler to run tasks (function, conditions) in an asynchronous fashion."""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
from enum import Enum
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from bec_lib.file_utils import get_full_file_path
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from bec_lib.utils.import_utils import lazy_import_from
|
||||||
|
from ophyd import Device, DeviceStatus, StatusBase
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bec_lib.messages import ScanStatusMessage
|
||||||
|
else:
|
||||||
|
# TODO: put back normal import when Pydantic gets faster
|
||||||
|
ScanStatusMessage = lazy_import_from("bec_lib.messages", ("ScanStatusMessage",))
|
||||||
|
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
set_async_exc = ctypes.pythonapi.PyThreadState_SetAsyncExc
|
||||||
|
|
||||||
|
|
||||||
|
class TaskState(str, Enum):
|
||||||
|
"""Possible task states"""
|
||||||
|
|
||||||
|
NOT_STARTED = "not_started"
|
||||||
|
RUNNING = "running"
|
||||||
|
TIMEOUT = "timeout"
|
||||||
|
ERROR = "error"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
KILLED = "killed"
|
||||||
|
|
||||||
|
|
||||||
|
class TaskKilledError(Exception):
|
||||||
|
"""Exception raised when a task thread is killed"""
|
||||||
|
|
||||||
|
|
||||||
|
class TaskStatus(DeviceStatus):
|
||||||
|
"""Thin wrapper around StatusBase to add information about tasks"""
|
||||||
|
|
||||||
|
def __init__(self, device: Device, *, timeout=None, settle_time=0, done=None, success=None):
|
||||||
|
super().__init__(
|
||||||
|
device=device, timeout=timeout, settle_time=settle_time, done=done, success=success
|
||||||
|
)
|
||||||
|
self._state = TaskState.NOT_STARTED
|
||||||
|
self._task_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exc(self) -> Exception:
|
||||||
|
"""Get the exception of the task"""
|
||||||
|
return self.exception()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> str:
|
||||||
|
"""Get the state of the task"""
|
||||||
|
return self._state.value
|
||||||
|
|
||||||
|
@state.setter
|
||||||
|
def state(self, value: TaskState):
|
||||||
|
self._state = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def task_id(self) -> bool:
|
||||||
|
"""Get the task ID"""
|
||||||
|
return self._task_id
|
||||||
|
|
||||||
|
|
||||||
|
class TaskHandler:
|
||||||
|
"""Handler to manage asynchronous tasks"""
|
||||||
|
|
||||||
|
def __init__(self, parent: Device):
|
||||||
|
"""Initialize the handler"""
|
||||||
|
self._tasks = {}
|
||||||
|
self._parent = parent
|
||||||
|
|
||||||
|
def submit_task(self, task: callable, run: bool = True) -> TaskStatus:
|
||||||
|
"""Submit a task to the task handler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task: The task to run.
|
||||||
|
run: Whether to run the task immediately.
|
||||||
|
"""
|
||||||
|
task_status = TaskStatus(device=self._parent)
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=self._wrap_task,
|
||||||
|
args=(task, task_status),
|
||||||
|
name=f"task {task_status.task_id}",
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
self._tasks.update({task_status.task_id: (task_status, thread)})
|
||||||
|
if run is True:
|
||||||
|
self.start_task(task_status)
|
||||||
|
return task_status
|
||||||
|
|
||||||
|
def start_task(self, task_status: TaskStatus) -> None:
|
||||||
|
"""Start a task,
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_status: The task status object.
|
||||||
|
"""
|
||||||
|
thread = self._tasks[task_status.task_id][1]
|
||||||
|
if thread.is_alive():
|
||||||
|
logger.warning(f"Task with ID {task_status.task_id} is already running.")
|
||||||
|
return
|
||||||
|
thread.start()
|
||||||
|
task_status.state = TaskState.RUNNING
|
||||||
|
|
||||||
|
def _wrap_task(self, task: callable, task_status: TaskStatus):
|
||||||
|
"""Wrap the task in a function"""
|
||||||
|
try:
|
||||||
|
task()
|
||||||
|
except TimeoutError as exc:
|
||||||
|
content = traceback.format_exc()
|
||||||
|
logger.warning(
|
||||||
|
(
|
||||||
|
f"Timeout Exception in task handler for task {task_status.task_id},"
|
||||||
|
f" Traceback: {content}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
task_status.set_exception(exc)
|
||||||
|
task_status.state = TaskState.TIMEOUT
|
||||||
|
except TaskKilledError as exc:
|
||||||
|
exc = exc.__class__(
|
||||||
|
f"Task {task_status.task_id} was killed. ThreadID:"
|
||||||
|
f" {self._tasks[task_status.task_id][1].ident}"
|
||||||
|
)
|
||||||
|
content = traceback.format_exc()
|
||||||
|
logger.warning(
|
||||||
|
(
|
||||||
|
f"TaskKilled Exception in task handler for task {task_status.task_id},"
|
||||||
|
f" Traceback: {content}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
task_status.set_exception(exc)
|
||||||
|
task_status.state = TaskState.KILLED
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
content = traceback.format_exc()
|
||||||
|
logger.warning(
|
||||||
|
f"Exception in task handler for task {task_status.task_id}, Traceback: {content}"
|
||||||
|
)
|
||||||
|
task_status.set_exception(exc)
|
||||||
|
task_status.state = TaskState.ERROR
|
||||||
|
else:
|
||||||
|
task_status.set_finished()
|
||||||
|
task_status.state = TaskState.COMPLETED
|
||||||
|
finally:
|
||||||
|
self._tasks.pop(task_status.task_id)
|
||||||
|
|
||||||
|
def kill_task(self, task_status: TaskStatus) -> None:
|
||||||
|
"""Kill the thread
|
||||||
|
|
||||||
|
task_status: The task status object.
|
||||||
|
"""
|
||||||
|
thread = self._tasks[task_status.task_id][1]
|
||||||
|
exception_cls = TaskKilledError
|
||||||
|
|
||||||
|
ident = ctypes.c_long(thread.ident)
|
||||||
|
exc = ctypes.py_object(exception_cls)
|
||||||
|
try:
|
||||||
|
res = set_async_exc(ident, exc)
|
||||||
|
if res == 0:
|
||||||
|
raise ValueError("Invalid thread ID")
|
||||||
|
elif res > 1:
|
||||||
|
set_async_exc(ident, None)
|
||||||
|
logger.warning(f"Exception raise while kille Thread {ident}; return value: {res}")
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
logger.warning(f"Exception raised while killing thread {ident}: {e}")
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""Shutdown all tasks of task handler"""
|
||||||
|
for info in self._tasks.values():
|
||||||
|
self.kill_task(info[0])
|
||||||
|
self._tasks.clear()
|
||||||
|
|
||||||
|
|
||||||
|
class FileHandler:
|
||||||
|
"""Utility class for file operations."""
|
||||||
|
|
||||||
|
def get_file_path(
|
||||||
|
self, scan_status_msg: ScanStatusMessage, name: str, create_dir: bool = True
|
||||||
|
) -> str:
|
||||||
|
"""Get the file path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
scan_info_msg: The scan info message.
|
||||||
|
name: The name of the file.
|
||||||
|
create_dir: Whether to create the directory.
|
||||||
|
"""
|
||||||
|
return get_full_file_path(scan_status_msg=scan_status_msg, name=name, create_dir=create_dir)
|
@ -6,200 +6,215 @@ import pytest
|
|||||||
from ophyd import DeviceStatus, Staged
|
from ophyd import DeviceStatus, Staged
|
||||||
from ophyd.utils.errors import RedundantStaging
|
from ophyd.utils.errors import RedundantStaging
|
||||||
|
|
||||||
from ophyd_devices.interfaces.base_classes.bec_device_base import BECDeviceBase, CustomPrepare
|
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||||
from ophyd_devices.utils.bec_scaninfo_mixin import BecScaninfoMixin
|
|
||||||
from ophyd_devices.utils.errors import DeviceStopError, DeviceTimeoutError
|
from ophyd_devices.utils.errors import DeviceStopError, DeviceTimeoutError
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def detector_base():
|
def detector_base():
|
||||||
yield BECDeviceBase(name="test_detector")
|
yield PSIDeviceBase(name="test_detector")
|
||||||
|
|
||||||
|
|
||||||
def test_detector_base_init(detector_base):
|
def test_detector_base_init(detector_base):
|
||||||
assert detector_base.stopped is False
|
assert detector_base.stopped is False
|
||||||
assert detector_base.name == "test_detector"
|
assert detector_base.name == "test_detector"
|
||||||
assert "base_path" in detector_base.filewriter.service_config
|
assert detector_base.staged == Staged.no
|
||||||
assert isinstance(detector_base.scaninfo, BecScaninfoMixin)
|
assert detector_base.destroyed == False
|
||||||
assert issubclass(detector_base.custom_prepare_cls, CustomPrepare)
|
|
||||||
|
|
||||||
|
|
||||||
def test_stage(detector_base):
|
def test_stage(detector_base):
|
||||||
detector_base._staged = Staged.yes
|
assert detector_base._staged == Staged.no
|
||||||
with pytest.raises(RedundantStaging):
|
|
||||||
detector_base.stage()
|
|
||||||
assert detector_base.stopped is False
|
assert detector_base.stopped is False
|
||||||
detector_base._staged = Staged.no
|
detector_base._staged = Staged.no
|
||||||
with (
|
with mock.patch.object(detector_base, "on_stage") as mock_on_stage:
|
||||||
mock.patch.object(detector_base.custom_prepare, "on_stage") as mock_on_stage,
|
|
||||||
mock.patch.object(detector_base.scaninfo, "load_scan_metadata") as mock_load_metadata,
|
|
||||||
):
|
|
||||||
rtr = detector_base.stage()
|
rtr = detector_base.stage()
|
||||||
assert isinstance(rtr, list)
|
assert isinstance(rtr, list)
|
||||||
mock_on_stage.assert_called_once()
|
assert mock_on_stage.called is True
|
||||||
mock_load_metadata.assert_called_once()
|
with pytest.raises(RedundantStaging):
|
||||||
|
detector_base.stage()
|
||||||
|
detector_base._staged = Staged.no
|
||||||
|
detector_base.stopped = True
|
||||||
|
detector_base.stage()
|
||||||
assert detector_base.stopped is False
|
assert detector_base.stopped is False
|
||||||
|
assert mock_on_stage.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_pre_scan(detector_base):
|
# def test_stage(detector_base):
|
||||||
with mock.patch.object(detector_base.custom_prepare, "on_pre_scan") as mock_on_pre_scan:
|
# detector_base._staged = Staged.yes
|
||||||
detector_base.pre_scan()
|
# with pytest.raises(RedundantStaging):
|
||||||
mock_on_pre_scan.assert_called_once()
|
# detector_base.stage()
|
||||||
|
# assert detector_base.stopped is False
|
||||||
|
# detector_base._staged = Staged.no
|
||||||
|
# with (
|
||||||
|
# mock.patch.object(detector_base.custom_prepare, "on_stage") as mock_on_stage,
|
||||||
|
# mock.patch.object(detector_base.scaninfo, "load_scan_metadata") as mock_load_metadata,
|
||||||
|
# ):
|
||||||
|
# rtr = detector_base.stage()
|
||||||
|
# assert isinstance(rtr, list)
|
||||||
|
# mock_on_stage.assert_called_once()
|
||||||
|
# mock_load_metadata.assert_called_once()
|
||||||
|
# assert detector_base.stopped is False
|
||||||
|
|
||||||
|
|
||||||
def test_trigger(detector_base):
|
# def test_pre_scan(detector_base):
|
||||||
status = DeviceStatus(detector_base)
|
# with mock.patch.object(detector_base.custom_prepare, "on_pre_scan") as mock_on_pre_scan:
|
||||||
with mock.patch.object(
|
# detector_base.pre_scan()
|
||||||
detector_base.custom_prepare, "on_trigger", side_effect=[None, status]
|
# mock_on_pre_scan.assert_called_once()
|
||||||
) as mock_on_trigger:
|
|
||||||
st = detector_base.trigger()
|
|
||||||
assert isinstance(st, DeviceStatus)
|
|
||||||
time.sleep(0.1)
|
|
||||||
assert st.done is True
|
|
||||||
st = detector_base.trigger()
|
|
||||||
assert st.done is False
|
|
||||||
assert id(st) == id(status)
|
|
||||||
|
|
||||||
|
|
||||||
def test_unstage(detector_base):
|
# def test_trigger(detector_base):
|
||||||
detector_base.stopped = True
|
# status = DeviceStatus(detector_base)
|
||||||
with (
|
# with mock.patch.object(
|
||||||
mock.patch.object(detector_base.custom_prepare, "on_unstage") as mock_on_unstage,
|
# detector_base.custom_prepare, "on_trigger", side_effect=[None, status]
|
||||||
mock.patch.object(detector_base, "check_scan_id") as mock_check_scan_id,
|
# ) as mock_on_trigger:
|
||||||
):
|
# st = detector_base.trigger()
|
||||||
rtr = detector_base.unstage()
|
# assert isinstance(st, DeviceStatus)
|
||||||
assert isinstance(rtr, list)
|
# time.sleep(0.1)
|
||||||
assert mock_check_scan_id.call_count == 1
|
# assert st.done is True
|
||||||
assert mock_on_unstage.call_count == 1
|
# st = detector_base.trigger()
|
||||||
detector_base.stopped = False
|
# assert st.done is False
|
||||||
rtr = detector_base.unstage()
|
# assert id(st) == id(status)
|
||||||
assert isinstance(rtr, list)
|
|
||||||
assert mock_check_scan_id.call_count == 2
|
|
||||||
assert mock_on_unstage.call_count == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_complete(detector_base):
|
# def test_unstage(detector_base):
|
||||||
status = DeviceStatus(detector_base)
|
# detector_base.stopped = True
|
||||||
with mock.patch.object(
|
# with (
|
||||||
detector_base.custom_prepare, "on_complete", side_effect=[None, status]
|
# mock.patch.object(detector_base.custom_prepare, "on_unstage") as mock_on_unstage,
|
||||||
) as mock_on_complete:
|
# mock.patch.object(detector_base, "check_scan_id") as mock_check_scan_id,
|
||||||
st = detector_base.complete()
|
# ):
|
||||||
assert isinstance(st, DeviceStatus)
|
# rtr = detector_base.unstage()
|
||||||
time.sleep(0.1)
|
# assert isinstance(rtr, list)
|
||||||
assert st.done is True
|
# assert mock_check_scan_id.call_count == 1
|
||||||
st = detector_base.complete()
|
# assert mock_on_unstage.call_count == 1
|
||||||
assert st.done is False
|
# detector_base.stopped = False
|
||||||
assert id(st) == id(status)
|
# rtr = detector_base.unstage()
|
||||||
|
# assert isinstance(rtr, list)
|
||||||
|
# assert mock_check_scan_id.call_count == 2
|
||||||
|
# assert mock_on_unstage.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_stop(detector_base):
|
# def test_complete(detector_base):
|
||||||
with mock.patch.object(detector_base.custom_prepare, "on_stop") as mock_on_stop:
|
# status = DeviceStatus(detector_base)
|
||||||
detector_base.stop()
|
# with mock.patch.object(
|
||||||
mock_on_stop.assert_called_once()
|
# detector_base.custom_prepare, "on_complete", side_effect=[None, status]
|
||||||
assert detector_base.stopped is True
|
# ) as mock_on_complete:
|
||||||
|
# st = detector_base.complete()
|
||||||
|
# assert isinstance(st, DeviceStatus)
|
||||||
|
# time.sleep(0.1)
|
||||||
|
# assert st.done is True
|
||||||
|
# st = detector_base.complete()
|
||||||
|
# assert st.done is False
|
||||||
|
# assert id(st) == id(status)
|
||||||
|
|
||||||
|
|
||||||
def test_check_scan_id(detector_base):
|
# def test_stop(detector_base):
|
||||||
detector_base.scaninfo.scan_id = "abcde"
|
# with mock.patch.object(detector_base.custom_prepare, "on_stop") as mock_on_stop:
|
||||||
detector_base.stopped = False
|
# detector_base.stop()
|
||||||
detector_base.check_scan_id()
|
# mock_on_stop.assert_called_once()
|
||||||
assert detector_base.stopped is True
|
# assert detector_base.stopped is True
|
||||||
detector_base.stopped = False
|
|
||||||
detector_base.check_scan_id()
|
|
||||||
assert detector_base.stopped is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_wait_for_signal(detector_base):
|
# def test_check_scan_id(detector_base):
|
||||||
my_value = False
|
# detector_base.scaninfo.scan_id = "abcde"
|
||||||
|
# detector_base.stopped = False
|
||||||
def my_callback():
|
# detector_base.check_scan_id()
|
||||||
return my_value
|
# assert detector_base.stopped is True
|
||||||
|
# detector_base.stopped = False
|
||||||
detector_base
|
# detector_base.check_scan_id()
|
||||||
status = detector_base.custom_prepare.wait_with_status(
|
# assert detector_base.stopped is False
|
||||||
[(my_callback, True)],
|
|
||||||
check_stopped=True,
|
|
||||||
timeout=5,
|
|
||||||
interval=0.01,
|
|
||||||
exception_on_timeout=None,
|
|
||||||
)
|
|
||||||
time.sleep(0.1)
|
|
||||||
assert status.done is False
|
|
||||||
# Check first that it is stopped when detector_base.stop() is called
|
|
||||||
detector_base.stop()
|
|
||||||
# some delay to allow the stop to take effect
|
|
||||||
time.sleep(0.15)
|
|
||||||
assert status.done is True
|
|
||||||
assert status.exception().args == DeviceStopError(f"{detector_base.name} was stopped").args
|
|
||||||
detector_base.stopped = False
|
|
||||||
status = detector_base.custom_prepare.wait_with_status(
|
|
||||||
[(my_callback, True)],
|
|
||||||
check_stopped=True,
|
|
||||||
timeout=5,
|
|
||||||
interval=0.01,
|
|
||||||
exception_on_timeout=None,
|
|
||||||
)
|
|
||||||
# Check that thread resolves when expected value is set
|
|
||||||
my_value = True
|
|
||||||
# some delay to allow the stop to take effect
|
|
||||||
time.sleep(0.15)
|
|
||||||
assert status.done is True
|
|
||||||
assert status.success is True
|
|
||||||
assert status.exception() is None
|
|
||||||
|
|
||||||
detector_base.stopped = False
|
|
||||||
# Check that wait for status runs into timeout with expectd exception
|
|
||||||
my_value = "random_value"
|
|
||||||
exception = TimeoutError("Timeout")
|
|
||||||
status = detector_base.custom_prepare.wait_with_status(
|
|
||||||
[(my_callback, True)],
|
|
||||||
check_stopped=True,
|
|
||||||
timeout=0.01,
|
|
||||||
interval=0.01,
|
|
||||||
exception_on_timeout=exception,
|
|
||||||
)
|
|
||||||
time.sleep(0.2)
|
|
||||||
assert status.done is True
|
|
||||||
assert id(status.exception()) == id(exception)
|
|
||||||
assert status.success is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_wait_for_signal_returns_exception(detector_base):
|
# def test_wait_for_signal(detector_base):
|
||||||
my_value = False
|
# my_value = False
|
||||||
|
|
||||||
def my_callback():
|
# def my_callback():
|
||||||
return my_value
|
# return my_value
|
||||||
|
|
||||||
# Check that wait for status runs into timeout with expectd exception
|
# detector_base
|
||||||
|
# status = detector_base.custom_prepare.wait_with_status(
|
||||||
|
# [(my_callback, True)],
|
||||||
|
# check_stopped=True,
|
||||||
|
# timeout=5,
|
||||||
|
# interval=0.01,
|
||||||
|
# exception_on_timeout=None,
|
||||||
|
# )
|
||||||
|
# time.sleep(0.1)
|
||||||
|
# assert status.done is False
|
||||||
|
# # Check first that it is stopped when detector_base.stop() is called
|
||||||
|
# detector_base.stop()
|
||||||
|
# # some delay to allow the stop to take effect
|
||||||
|
# time.sleep(0.15)
|
||||||
|
# assert status.done is True
|
||||||
|
# assert status.exception().args == DeviceStopError(f"{detector_base.name} was stopped").args
|
||||||
|
# detector_base.stopped = False
|
||||||
|
# status = detector_base.custom_prepare.wait_with_status(
|
||||||
|
# [(my_callback, True)],
|
||||||
|
# check_stopped=True,
|
||||||
|
# timeout=5,
|
||||||
|
# interval=0.01,
|
||||||
|
# exception_on_timeout=None,
|
||||||
|
# )
|
||||||
|
# # Check that thread resolves when expected value is set
|
||||||
|
# my_value = True
|
||||||
|
# # some delay to allow the stop to take effect
|
||||||
|
# time.sleep(0.15)
|
||||||
|
# assert status.done is True
|
||||||
|
# assert status.success is True
|
||||||
|
# assert status.exception() is None
|
||||||
|
|
||||||
exception = TimeoutError("Timeout")
|
# detector_base.stopped = False
|
||||||
status = detector_base.custom_prepare.wait_with_status(
|
# # Check that wait for status runs into timeout with expectd exception
|
||||||
[(my_callback, True)],
|
# my_value = "random_value"
|
||||||
check_stopped=True,
|
# exception = TimeoutError("Timeout")
|
||||||
timeout=0.01,
|
# status = detector_base.custom_prepare.wait_with_status(
|
||||||
interval=0.01,
|
# [(my_callback, True)],
|
||||||
exception_on_timeout=exception,
|
# check_stopped=True,
|
||||||
)
|
# timeout=0.01,
|
||||||
time.sleep(0.2)
|
# interval=0.01,
|
||||||
assert status.done is True
|
# exception_on_timeout=exception,
|
||||||
assert id(status.exception()) == id(exception)
|
# )
|
||||||
assert status.success is False
|
# time.sleep(0.2)
|
||||||
|
# assert status.done is True
|
||||||
|
# assert id(status.exception()) == id(exception)
|
||||||
|
# assert status.success is False
|
||||||
|
|
||||||
detector_base.stopped = False
|
|
||||||
# Check that standard exception is thrown
|
# def test_wait_for_signal_returns_exception(detector_base):
|
||||||
status = detector_base.custom_prepare.wait_with_status(
|
# my_value = False
|
||||||
[(my_callback, True)],
|
|
||||||
check_stopped=True,
|
# def my_callback():
|
||||||
timeout=0.01,
|
# return my_value
|
||||||
interval=0.01,
|
|
||||||
exception_on_timeout=None,
|
# # Check that wait for status runs into timeout with expectd exception
|
||||||
)
|
|
||||||
time.sleep(0.2)
|
# exception = TimeoutError("Timeout")
|
||||||
assert status.done is True
|
# status = detector_base.custom_prepare.wait_with_status(
|
||||||
assert (
|
# [(my_callback, True)],
|
||||||
status.exception().args
|
# check_stopped=True,
|
||||||
== DeviceTimeoutError(
|
# timeout=0.01,
|
||||||
f"Timeout error for {detector_base.name} while waiting for signals {[(my_callback, True)]}"
|
# interval=0.01,
|
||||||
).args
|
# exception_on_timeout=exception,
|
||||||
)
|
# )
|
||||||
assert status.success is False
|
# time.sleep(0.2)
|
||||||
|
# assert status.done is True
|
||||||
|
# assert id(status.exception()) == id(exception)
|
||||||
|
# assert status.success is False
|
||||||
|
|
||||||
|
# detector_base.stopped = False
|
||||||
|
# # Check that standard exception is thrown
|
||||||
|
# status = detector_base.custom_prepare.wait_with_status(
|
||||||
|
# [(my_callback, True)],
|
||||||
|
# check_stopped=True,
|
||||||
|
# timeout=0.01,
|
||||||
|
# interval=0.01,
|
||||||
|
# exception_on_timeout=None,
|
||||||
|
# )
|
||||||
|
# time.sleep(0.2)
|
||||||
|
# assert status.done is True
|
||||||
|
# assert (
|
||||||
|
# status.exception().args
|
||||||
|
# == DeviceTimeoutError(
|
||||||
|
# f"Timeout error for {detector_base.name} while waiting for signals {[(my_callback, True)]}"
|
||||||
|
# ).args
|
||||||
|
# )
|
||||||
|
# assert status.success is False
|
||||||
|
@ -17,7 +17,6 @@ from ophyd import Device, Signal
|
|||||||
from ophyd.status import wait as status_wait
|
from ophyd.status import wait as status_wait
|
||||||
|
|
||||||
from ophyd_devices.interfaces.protocols.bec_protocols import (
|
from ophyd_devices.interfaces.protocols.bec_protocols import (
|
||||||
BECBaseProtocol,
|
|
||||||
BECDeviceProtocol,
|
BECDeviceProtocol,
|
||||||
BECFlyerProtocol,
|
BECFlyerProtocol,
|
||||||
BECPositionerProtocol,
|
BECPositionerProtocol,
|
||||||
@ -31,6 +30,7 @@ from ophyd_devices.sim.sim_positioner import SimLinearTrajectoryPositioner, SimP
|
|||||||
from ophyd_devices.sim.sim_signals import ReadOnlySignal
|
from ophyd_devices.sim.sim_signals import ReadOnlySignal
|
||||||
from ophyd_devices.sim.sim_utils import H5Writer, LinearTrajectory
|
from ophyd_devices.sim.sim_utils import H5Writer, LinearTrajectory
|
||||||
from ophyd_devices.sim.sim_waveform import SimWaveform
|
from ophyd_devices.sim.sim_waveform import SimWaveform
|
||||||
|
from ophyd_devices.tests.utils import get_mock_scan_info
|
||||||
from ophyd_devices.utils.bec_device_base import BECDevice, BECDeviceBase
|
from ophyd_devices.utils.bec_device_base import BECDevice, BECDeviceBase
|
||||||
|
|
||||||
|
|
||||||
@ -423,7 +423,6 @@ def test_h5proxy(h5proxy_fixture):
|
|||||||
)
|
)
|
||||||
camera._registered_proxies.update({h5proxy.name: camera.image.name})
|
camera._registered_proxies.update({h5proxy.name: camera.image.name})
|
||||||
camera.sim.params = {"noise": "none", "noise_multiplier": 0}
|
camera.sim.params = {"noise": "none", "noise_multiplier": 0}
|
||||||
camera.scaninfo.sim_mode = True
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
camera.image_shape.set(data.shape[1:])
|
camera.image_shape.set(data.shape[1:])
|
||||||
camera.stage()
|
camera.stage()
|
||||||
@ -544,15 +543,15 @@ def test_cam_stage_h5writer(camera):
|
|||||||
mock.patch.object(camera, "h5_writer") as mock_h5_writer,
|
mock.patch.object(camera, "h5_writer") as mock_h5_writer,
|
||||||
mock.patch.object(camera, "_run_subs") as mock_run_subs,
|
mock.patch.object(camera, "_run_subs") as mock_run_subs,
|
||||||
):
|
):
|
||||||
camera.scaninfo.num_points = 10
|
camera.scan_info.msg.num_points = 10
|
||||||
camera.scaninfo.frames_per_trigger = 1
|
camera.scan_info.msg.scan_parameters["frames_per_trigger"] = 1
|
||||||
camera.scaninfo.exp_time = 1
|
camera.scan_info.msg.scan_parameters["exp_time"] = 1
|
||||||
camera.stage()
|
camera.stage()
|
||||||
assert mock_h5_writer.on_stage.call_count == 0
|
assert mock_h5_writer.on_stage.call_count == 0
|
||||||
camera.unstage()
|
camera.unstage()
|
||||||
camera.write_to_disk.put(True)
|
camera.write_to_disk.put(True)
|
||||||
camera.stage()
|
camera.stage()
|
||||||
calls = [mock.call(file_path="", h5_entry="/entry/data/data")]
|
calls = [mock.call(file_path="./data/test_file_camera.h5", h5_entry="/entry/data/data")]
|
||||||
assert mock_h5_writer.on_stage.mock_calls == calls
|
assert mock_h5_writer.on_stage.mock_calls == calls
|
||||||
# mock_h5_writer.prepare
|
# mock_h5_writer.prepare
|
||||||
|
|
||||||
@ -622,17 +621,17 @@ def test_async_monitor_stage(async_monitor):
|
|||||||
|
|
||||||
def test_async_monitor_prep_random_interval(async_monitor):
|
def test_async_monitor_prep_random_interval(async_monitor):
|
||||||
"""Test the stage method of SimMonitorAsync."""
|
"""Test the stage method of SimMonitorAsync."""
|
||||||
async_monitor.custom_prepare.prep_random_interval()
|
async_monitor.prep_random_interval()
|
||||||
assert async_monitor.custom_prepare._counter == 0
|
assert async_monitor._counter == 0
|
||||||
assert async_monitor.current_trigger.get() == 0
|
assert async_monitor.current_trigger.get() == 0
|
||||||
assert 0 < async_monitor.custom_prepare._random_send_interval < 10
|
assert 0 < async_monitor._random_send_interval < 10
|
||||||
|
|
||||||
|
|
||||||
def test_async_monitor_complete(async_monitor):
|
def test_async_monitor_complete(async_monitor):
|
||||||
"""Test the on_complete method of SimMonitorAsync."""
|
"""Test the on_complete method of SimMonitorAsync."""
|
||||||
with (
|
with (
|
||||||
mock.patch.object(async_monitor.custom_prepare, "_send_data_to_bec") as mock_send,
|
mock.patch.object(async_monitor, "_send_data_to_bec") as mock_send,
|
||||||
mock.patch.object(async_monitor.custom_prepare, "prep_random_interval") as mock_prep,
|
mock.patch.object(async_monitor, "prep_random_interval") as mock_prep,
|
||||||
):
|
):
|
||||||
status = async_monitor.complete()
|
status = async_monitor.complete()
|
||||||
status_wait(status)
|
status_wait(status)
|
||||||
@ -649,11 +648,11 @@ def test_async_monitor_complete(async_monitor):
|
|||||||
|
|
||||||
def test_async_mon_on_trigger(async_monitor):
|
def test_async_mon_on_trigger(async_monitor):
|
||||||
"""Test the on_trigger method of SimMonitorAsync."""
|
"""Test the on_trigger method of SimMonitorAsync."""
|
||||||
with (mock.patch.object(async_monitor.custom_prepare, "_send_data_to_bec") as mock_send,):
|
with (mock.patch.object(async_monitor, "_send_data_to_bec") as mock_send,):
|
||||||
async_monitor.custom_prepare.on_stage()
|
async_monitor.on_stage()
|
||||||
upper_limit = async_monitor.custom_prepare._random_send_interval
|
upper_limit = async_monitor._random_send_interval
|
||||||
for ii in range(1, upper_limit + 1):
|
for ii in range(1, upper_limit + 1):
|
||||||
status = async_monitor.custom_prepare.on_trigger()
|
status = async_monitor.on_trigger()
|
||||||
status_wait(status)
|
status_wait(status)
|
||||||
assert async_monitor.current_trigger.get() == ii
|
assert async_monitor.current_trigger.get() == ii
|
||||||
assert mock_send.call_count == 1
|
assert mock_send.call_count == 1
|
||||||
@ -661,10 +660,10 @@ def test_async_mon_on_trigger(async_monitor):
|
|||||||
|
|
||||||
def test_async_mon_send_data_to_bec(async_monitor):
|
def test_async_mon_send_data_to_bec(async_monitor):
|
||||||
"""Test the _send_data_to_bec method of SimMonitorAsync."""
|
"""Test the _send_data_to_bec method of SimMonitorAsync."""
|
||||||
async_monitor.scaninfo.scan_msg = SimpleNamespace(metadata={})
|
async_monitor.scan_info = get_mock_scan_info()
|
||||||
async_monitor.data_buffer.update({"value": [0, 5], "timestamp": [0, 0]})
|
async_monitor.data_buffer.update({"value": [0, 5], "timestamp": [0, 0]})
|
||||||
with mock.patch.object(async_monitor.connector, "xadd") as mock_xadd:
|
with mock.patch.object(async_monitor.connector, "xadd") as mock_xadd:
|
||||||
async_monitor.custom_prepare._send_data_to_bec()
|
async_monitor._send_data_to_bec()
|
||||||
dev_msg = messages.DeviceMessage(
|
dev_msg = messages.DeviceMessage(
|
||||||
signals={async_monitor.readback.name: async_monitor.data_buffer},
|
signals={async_monitor.readback.name: async_monitor.data_buffer},
|
||||||
metadata={"async_update": async_monitor.async_update.get()},
|
metadata={"async_update": async_monitor.async_update.get()},
|
||||||
@ -673,10 +672,10 @@ def test_async_mon_send_data_to_bec(async_monitor):
|
|||||||
call = [
|
call = [
|
||||||
mock.call(
|
mock.call(
|
||||||
MessageEndpoints.device_async_readback(
|
MessageEndpoints.device_async_readback(
|
||||||
scan_id=async_monitor.scaninfo.scan_id, device=async_monitor.name
|
scan_id=async_monitor.scan_info.msg.scan_id, device=async_monitor.name
|
||||||
),
|
),
|
||||||
{"data": dev_msg},
|
{"data": dev_msg},
|
||||||
expire=async_monitor.custom_prepare._stream_ttl,
|
expire=async_monitor._stream_ttl,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
assert mock_xadd.mock_calls == call
|
assert mock_xadd.mock_calls == call
|
||||||
@ -711,8 +710,8 @@ def test_waveform(waveform):
|
|||||||
# Now also test the async readback
|
# Now also test the async readback
|
||||||
mock_connector = waveform.connector = mock.MagicMock()
|
mock_connector = waveform.connector = mock.MagicMock()
|
||||||
mock_run_subs = waveform._run_subs = mock.MagicMock()
|
mock_run_subs = waveform._run_subs = mock.MagicMock()
|
||||||
waveform.scaninfo.scan_msg = SimpleNamespace(metadata={})
|
waveform.scan_info = get_mock_scan_info()
|
||||||
waveform.scaninfo.scan_id = "test"
|
waveform.scan_info.msg.scan_id = "test"
|
||||||
status = waveform.trigger()
|
status = waveform.trigger()
|
||||||
timer = 0
|
timer = 0
|
||||||
while not status.done:
|
while not status.done:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user