|
|
|
|
@@ -6,16 +6,20 @@ Created on Wed Dec 6 11:33:54 2023
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind
|
|
|
|
|
from ophyd.status import SubscriptionStatus
|
|
|
|
|
from ophyd.status import SubscriptionStatus, DeviceStatus
|
|
|
|
|
import time
|
|
|
|
|
from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase as PSIDeviceBase
|
|
|
|
|
from ophyd_devices.interfaces.base_classes.psi_detector_base import CustomDetectorMixin as CustomDeviceMixin
|
|
|
|
|
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
|
|
|
|
CustomDetectorMixin as CustomDeviceMixin,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from bec_lib import bec_logger
|
|
|
|
|
|
|
|
|
|
logger = bec_logger.logger
|
|
|
|
|
except ModuleNotFoundError:
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("PcoEdgeCam")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -24,42 +28,43 @@ class PcoEdgeCameraMixin(CustomDeviceMixin):
|
|
|
|
|
|
|
|
|
|
This class will be called by the custom_prepare_cls attribute of the detector class.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def on_stage(self) -> None:
|
|
|
|
|
"""Configure and arm PCO.Edge camera for acquisition
|
|
|
|
|
"""
|
|
|
|
|
"""Configure and arm PCO.Edge camera for acquisition"""
|
|
|
|
|
|
|
|
|
|
# PCO can finish a run without explicit unstaging
|
|
|
|
|
if self.parent.state not in ("IDLE"):
|
|
|
|
|
logger.warning(f"Trying to stage the camera from state {self.parent.state}, unstaging it first!")
|
|
|
|
|
logger.warning(
|
|
|
|
|
f"Trying to stage the camera from state {self.parent.state}, unstaging it first!"
|
|
|
|
|
)
|
|
|
|
|
self.parent.unstage()
|
|
|
|
|
time.sleep(0.5)
|
|
|
|
|
|
|
|
|
|
# Fish out our configuration from scaninfo (via explicit or generic addressing)
|
|
|
|
|
scanparam = self.parent.scaninfo.scan_msg.info
|
|
|
|
|
d = {}
|
|
|
|
|
if 'kwargs' in scanparam:
|
|
|
|
|
scanargs = scanparam['kwargs']
|
|
|
|
|
if 'num_images_total' in scanargs and scanargs['num_images_total'] is not None:
|
|
|
|
|
d['images_total'] = scanargs['num_images_total']
|
|
|
|
|
if 'image_width' in scanargs and scanargs['image_width'] is not None:
|
|
|
|
|
d['image_width'] = scanargs['image_width']
|
|
|
|
|
if 'image_height' in scanargs and scanargs['image_height'] is not None:
|
|
|
|
|
d['image_height'] = scanargs['image_height']
|
|
|
|
|
if 'exp_time' in scanargs and scanargs['exp_time'] is not None:
|
|
|
|
|
d['exposure_time_ms'] = scanargs['exp_time']
|
|
|
|
|
if 'exp_period' in scanargs and scanargs['exp_period'] is not None:
|
|
|
|
|
d['exposure_period_ms'] = scanargs['exp_period']
|
|
|
|
|
if "kwargs" in scanparam:
|
|
|
|
|
scanargs = scanparam["kwargs"]
|
|
|
|
|
if "exp_burst" in scanargs and scanargs["exp_burst"] is not None:
|
|
|
|
|
d["exposure_num_burst"] = scanargs["exp_burst"]
|
|
|
|
|
if "image_width" in scanargs and scanargs["image_width"] is not None:
|
|
|
|
|
d["image_width"] = scanargs["image_width"]
|
|
|
|
|
if "image_height" in scanargs and scanargs["image_height"] is not None:
|
|
|
|
|
d["image_height"] = scanargs["image_height"]
|
|
|
|
|
if "exp_time" in scanargs and scanargs["exp_time"] is not None:
|
|
|
|
|
d["exposure_time_ms"] = scanargs["exp_time"]
|
|
|
|
|
if "exp_period" in scanargs and scanargs["exp_period"] is not None:
|
|
|
|
|
d["exposure_period_ms"] = scanargs["exp_period"]
|
|
|
|
|
# if 'exp_burst' in scanargs and scanargs['exp_burst'] is not None:
|
|
|
|
|
# d['exposure_num_burst'] = scanargs['exp_burst']
|
|
|
|
|
# if 'acq_mode' in scanargs and scanargs['acq_mode'] is not None:
|
|
|
|
|
# d['acq_mode'] = scanargs['acq_mode']
|
|
|
|
|
# elif self.parent.scaninfo.scan_type == "step":
|
|
|
|
|
# d['acq_mode'] = "default"
|
|
|
|
|
if 'pco_store_mode' in scanargs and scanargs['pco_store_mode'] is not None:
|
|
|
|
|
d['store_mode'] = scanargs['pco_store_mode']
|
|
|
|
|
if 'pco_data_format' in scanargs and scanargs['pco_data_format'] is not None:
|
|
|
|
|
d['data_format'] = scanargs['pco_data_format']
|
|
|
|
|
|
|
|
|
|
if "pco_store_mode" in scanargs and scanargs["pco_store_mode"] is not None:
|
|
|
|
|
d["store_mode"] = scanargs["pco_store_mode"]
|
|
|
|
|
if "pco_data_format" in scanargs and scanargs["pco_data_format"] is not None:
|
|
|
|
|
d["data_format"] = scanargs["pco_data_format"]
|
|
|
|
|
|
|
|
|
|
# Perform bluesky-style configuration
|
|
|
|
|
if len(d) > 0:
|
|
|
|
|
@@ -70,29 +75,86 @@ class PcoEdgeCameraMixin(CustomDeviceMixin):
|
|
|
|
|
self.parent.bluestage()
|
|
|
|
|
|
|
|
|
|
def on_unstage(self) -> None:
|
|
|
|
|
"""Disarm the PCO.Edge camera
|
|
|
|
|
"""
|
|
|
|
|
"""Disarm the PCO.Edge camera"""
|
|
|
|
|
self.parent.blueunstage()
|
|
|
|
|
|
|
|
|
|
def on_stop(self) -> None:
|
|
|
|
|
"""Stop the PCO.Edge camera
|
|
|
|
|
"""
|
|
|
|
|
"""Stop the PCO.Edge camera"""
|
|
|
|
|
self.parent.blueunstage()
|
|
|
|
|
|
|
|
|
|
def on_trigger(self) -> None | DeviceStatus:
|
|
|
|
|
"""Trigger mode operation
|
|
|
|
|
|
|
|
|
|
Use it to repeatedly record a fixed number of frames and send it to stdDAQ. The method waits
|
|
|
|
|
for the acquisition and data transfer to complete.
|
|
|
|
|
|
|
|
|
|
NOTE: Maciej confirmed that sparse data is no problem to the stdDAQ.
|
|
|
|
|
TODO: Optimize data transfer to launch at end and check completion at the beginning.
|
|
|
|
|
"""
|
|
|
|
|
logger.warning(f"triggering PCO")
|
|
|
|
|
# Ensure that previous data transfer finished
|
|
|
|
|
# def sentIt(*args, value, timestamp, **kwargs):
|
|
|
|
|
# return value==0
|
|
|
|
|
# status = SubscriptionStatus(self.parent.file_savebusy, sentIt, timeout=120)
|
|
|
|
|
# status.wait()
|
|
|
|
|
|
|
|
|
|
# Not sure if it always sends the first batch of images or the newest
|
|
|
|
|
def didWeReset(*args, old_value, value, timestamp, **kwargs):
|
|
|
|
|
return (value < old_value) or (value == 0)
|
|
|
|
|
|
|
|
|
|
self.parent.buffer_clear.set(1).wait()
|
|
|
|
|
status = SubscriptionStatus(self.parent.buffer_used, didWeReset, timeout=5)
|
|
|
|
|
status.wait()
|
|
|
|
|
|
|
|
|
|
t_expected = (
|
|
|
|
|
self.parent.acquire_time.get() + self.parent.acquire_delay.get()
|
|
|
|
|
) * self.parent.file_savestop.get()
|
|
|
|
|
|
|
|
|
|
# Wait until the buffer fills up with enough images
|
|
|
|
|
def areWeDoneYet(*args, old_value, value, timestamp, **kwargs):
|
|
|
|
|
num_target = self.parent.file_savestop.get()
|
|
|
|
|
# logger.warning(f"{value} of {num_target}")
|
|
|
|
|
return bool(value >= num_target)
|
|
|
|
|
|
|
|
|
|
status = SubscriptionStatus(
|
|
|
|
|
self.parent.buffer_used, areWeDoneYet, timeout=max(5, 5 * t_expected), settle_time=0.2
|
|
|
|
|
)
|
|
|
|
|
status.wait()
|
|
|
|
|
|
|
|
|
|
# Then start file transfer (need to get the save busy flag update)
|
|
|
|
|
# self.parent.file_transfer.set(1, settle_time=0.2).wait()
|
|
|
|
|
self.parent.file_transfer.set(1).wait()
|
|
|
|
|
|
|
|
|
|
# And wait until the images have been sent
|
|
|
|
|
# NOTE: this does not wait for new value, the first check will be
|
|
|
|
|
# against values from the previous cycle, i.e. pass automatically.
|
|
|
|
|
t_start = time.time()
|
|
|
|
|
|
|
|
|
|
def haveWeSentIt(*args, old_value, value, timestamp, **kwargs):
|
|
|
|
|
t_elapsed = timestamp - t_start
|
|
|
|
|
# logger.warning(f"{old_value}\t{value}\t{t_elapsed}")
|
|
|
|
|
return old_value == 1 and value == 0 and t_elapsed > 0
|
|
|
|
|
|
|
|
|
|
status = SubscriptionStatus(
|
|
|
|
|
self.parent.file_savebusy, haveWeSentIt, timeout=120, settle_time=0.2
|
|
|
|
|
)
|
|
|
|
|
status.wait()
|
|
|
|
|
logger.warning(f"done PCO")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HelgeCameraBase(PSIDeviceBase):
|
|
|
|
|
"""Ophyd baseclass for Helge camera IOCs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
This class provides wrappers for Helge's camera IOCs around SwissFEL and
|
|
|
|
|
for high performance SLS 2.0 cameras. The IOC's operation is a bit arcane
|
|
|
|
|
and there are different versions and cameras all around. So this device
|
|
|
|
|
and there are different versions and cameras all around. So this device
|
|
|
|
|
only covers the absolute basics.
|
|
|
|
|
|
|
|
|
|
Probably the most important part is the configuration state machine. As
|
|
|
|
|
the SET_PARAMS takes care of buffer allocations it might take some time,
|
|
|
|
|
as well as a full re-configuration is required every time we change the
|
|
|
|
|
binning, roi, etc... This is automatically performed upon starting an
|
|
|
|
|
exposure (if it heven't been done before).
|
|
|
|
|
as well as a full re-configuration is required every time we change the
|
|
|
|
|
binning, roi, etc... This is automatically performed upon starting an
|
|
|
|
|
exposure (if it heven't been done before).
|
|
|
|
|
|
|
|
|
|
The status flag state machine during re-configuration is:
|
|
|
|
|
BUSY low, SET low -> BUSY high, SET low -> BUSY low, SET high -> BUSY low, SET low
|
|
|
|
|
@@ -112,12 +174,12 @@ class HelgeCameraBase(PSIDeviceBase):
|
|
|
|
|
Note that in FIFO mode buffer reads are destructive, to prevent this, we don't have EPICS preview
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# ########################################################################
|
|
|
|
|
# General hardware info (in AD nomenclature)
|
|
|
|
|
manufacturer = Component(EpicsSignalRO, "QUERY", kind=Kind.config, doc="Camera model info")
|
|
|
|
|
model = Component(EpicsSignalRO, "BOARD", kind=Kind.omitted, doc="Camera board info")
|
|
|
|
|
model = Component(EpicsSignalRO, "BOARD", kind=Kind.omitted, doc="Camera board info")
|
|
|
|
|
|
|
|
|
|
# ########################################################################
|
|
|
|
|
# Acquisition commands
|
|
|
|
|
@@ -125,30 +187,41 @@ class HelgeCameraBase(PSIDeviceBase):
|
|
|
|
|
|
|
|
|
|
# ########################################################################
|
|
|
|
|
# Acquisition configuration (in AD nomenclature)
|
|
|
|
|
acquire_time = Component(EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config)
|
|
|
|
|
acquire_delay = Component(EpicsSignal, "DELAY", put_complete=True, auto_monitor=True, kind=Kind.config)
|
|
|
|
|
trigger_mode = Component(EpicsSignal, "TRIGGER", put_complete=True, auto_monitor=True, kind=Kind.config)
|
|
|
|
|
acquire_time = Component(
|
|
|
|
|
EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config
|
|
|
|
|
)
|
|
|
|
|
acquire_delay = Component(
|
|
|
|
|
EpicsSignal, "DELAY", put_complete=True, auto_monitor=True, kind=Kind.config
|
|
|
|
|
)
|
|
|
|
|
trigger_mode = Component(
|
|
|
|
|
EpicsSignal, "TRIGGER", put_complete=True, auto_monitor=True, kind=Kind.config
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ########################################################################
|
|
|
|
|
# Image size configuration (in AD nomenclature)
|
|
|
|
|
bin_x = Component(EpicsSignal, "BINX", put_complete=True, auto_monitor=True, kind=Kind.config)
|
|
|
|
|
bin_y = Component(EpicsSignal, "BINY", put_complete=True, auto_monitor=True, kind=Kind.config)
|
|
|
|
|
array_size_x = Component(EpicsSignalRO, "WIDTH", auto_monitor=True, kind=Kind.config, doc="Final image width")
|
|
|
|
|
array_size_y = Component(EpicsSignalRO, "HEIGHT", auto_monitor=True, kind=Kind.config, doc="Final image height")
|
|
|
|
|
array_size_x = Component(
|
|
|
|
|
EpicsSignalRO, "WIDTH", auto_monitor=True, kind=Kind.config, doc="Final image width"
|
|
|
|
|
)
|
|
|
|
|
array_size_y = Component(
|
|
|
|
|
EpicsSignalRO, "HEIGHT", auto_monitor=True, kind=Kind.config, doc="Final image height"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ########################################################################
|
|
|
|
|
# General hardware info
|
|
|
|
|
camError = Component(EpicsSignalRO, "ERRCODE", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
camWarning = Component(EpicsSignalRO, "WARNCODE", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
camWarning = Component(EpicsSignalRO, "WARNCODE", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
|
|
|
|
|
# ########################################################################
|
|
|
|
|
# Buffer configuration
|
|
|
|
|
# Buffer configuration
|
|
|
|
|
bufferRecMode = Component(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
bufferStoreMode = Component(EpicsSignal, "STOREMODE", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
fileRecMode = Component(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
|
|
|
|
|
buffer_used = Component(EpicsSignalRO, "PIC_BUFFER", auto_monitor=True, kind=Kind.normal)
|
|
|
|
|
buffer_size = Component(EpicsSignalRO, "PIC_MAX", auto_monitor=True, kind=Kind.normal)
|
|
|
|
|
buffer_clear = Component(EpicsSignal, "CLEARMEM", put_complete=True, kind=Kind.omitted)
|
|
|
|
|
|
|
|
|
|
# ########################################################################
|
|
|
|
|
# File saving interface
|
|
|
|
|
@@ -158,15 +231,18 @@ class HelgeCameraBase(PSIDeviceBase):
|
|
|
|
|
file_savestop = Component(EpicsSignal, "SAVESTOP", put_complete=True, kind=Kind.config)
|
|
|
|
|
file_format = Component(EpicsSignal, "FILEFORMAT", put_complete=True, kind=Kind.config)
|
|
|
|
|
file_transfer = Component(EpicsSignal, "FTRANSFER", put_complete=True, kind=Kind.config)
|
|
|
|
|
file_savebusy = Component(EpicsSignalRO, "FILESAVEBUSY", auto_monitor=True, kind=Kind.normal)
|
|
|
|
|
|
|
|
|
|
# ########################################################################
|
|
|
|
|
# Configuration state maschine with separate transition states
|
|
|
|
|
camStatusCode = Component(EpicsSignalRO, "STATUSCODE", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
camSetParam = Component(EpicsSignal, "SET_PARAM", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
camSetParamBusy = Component(EpicsSignalRO, "BUSY_SET_PARAM", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
camSetParamBusy = Component(
|
|
|
|
|
EpicsSignalRO, "BUSY_SET_PARAM", auto_monitor=True, kind=Kind.config
|
|
|
|
|
)
|
|
|
|
|
camCamera = Component(EpicsSignalRO, "CAMERA", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
camCameraBusy = Component(EpicsSignalRO, "BUSY_CAMERA", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
camInit= Component(EpicsSignalRO, "INIT", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
camInit = Component(EpicsSignalRO, "INIT", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
camInitBusy = Component(EpicsSignalRO, "BUSY_INIT", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
|
|
|
|
|
# ########################################################################
|
|
|
|
|
@@ -175,22 +251,24 @@ class HelgeCameraBase(PSIDeviceBase):
|
|
|
|
|
|
|
|
|
|
# ########################################################################
|
|
|
|
|
# Misc PVs
|
|
|
|
|
#camRemoval = Component(EpicsSignalRO, "REMOVAL", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
camStateString = Component(EpicsSignalRO, "SS_CAMERA", string=True, auto_monitor=True, kind=Kind.config)
|
|
|
|
|
# camRemoval = Component(EpicsSignalRO, "REMOVAL", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
camStateString = Component(
|
|
|
|
|
EpicsSignalRO, "SS_CAMERA", string=True, auto_monitor=True, kind=Kind.config
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def state(self) -> str:
|
|
|
|
|
""" Single word camera state"""
|
|
|
|
|
"""Single word camera state"""
|
|
|
|
|
if self.camSetParamBusy.value:
|
|
|
|
|
return "BUSY"
|
|
|
|
|
if self.camStatusCode.value==2 and self.camInit.value==1:
|
|
|
|
|
return "IDLE"
|
|
|
|
|
if self.camStatusCode.value==6 and self.camInit.value==1:
|
|
|
|
|
return "BUSY"
|
|
|
|
|
if self.camStatusCode.value == 2 and self.camInit.value == 1:
|
|
|
|
|
return "IDLE"
|
|
|
|
|
if self.camStatusCode.value == 6 and self.camInit.value == 1:
|
|
|
|
|
return "RUNNING"
|
|
|
|
|
#if self.camRemoval.value==0 and self.camInit.value==0:
|
|
|
|
|
if self.camInit.value==0:
|
|
|
|
|
# if self.camRemoval.value==0 and self.camInit.value==0:
|
|
|
|
|
if self.camInit.value == 0:
|
|
|
|
|
return "OFFLINE"
|
|
|
|
|
#if self.camRemoval.value:
|
|
|
|
|
# if self.camRemoval.value:
|
|
|
|
|
# return "REMOVED"
|
|
|
|
|
return "UNKNOWN"
|
|
|
|
|
|
|
|
|
|
@@ -199,103 +277,113 @@ class HelgeCameraBase(PSIDeviceBase):
|
|
|
|
|
raise ReadOnlyError("State is a ReadOnly property")
|
|
|
|
|
|
|
|
|
|
def configure(self, d: dict = {}) -> tuple:
|
|
|
|
|
""" Configure the base Helge camera device
|
|
|
|
|
|
|
|
|
|
Parameters as 'd' dictionary
|
|
|
|
|
----------------------------
|
|
|
|
|
num_images : int
|
|
|
|
|
Number of images to be taken during each scan. Meaning depends on
|
|
|
|
|
store mode.
|
|
|
|
|
exposure_time_ms : float
|
|
|
|
|
Exposure time [ms], usually gets set back to 20 ms
|
|
|
|
|
exposure_period_ms : float
|
|
|
|
|
Exposure period [ms], up to 200 ms.
|
|
|
|
|
store_mode : str
|
|
|
|
|
Buffer operation mode
|
|
|
|
|
*'Recorder' to record in buffer
|
|
|
|
|
*'FIFO buffer' for continous streaming
|
|
|
|
|
data_format : str
|
|
|
|
|
Usually set to 'ZEROMQ'
|
|
|
|
|
"""Configure the base Helge camera device
|
|
|
|
|
|
|
|
|
|
Parameters as 'd' dictionary
|
|
|
|
|
----------------------------
|
|
|
|
|
num_images : int
|
|
|
|
|
Number of images to be taken during each scan. Meaning depends on
|
|
|
|
|
store mode.
|
|
|
|
|
exposure_time_ms : float
|
|
|
|
|
Exposure time [ms], usually gets set back to 20 ms
|
|
|
|
|
exposure_period_ms : float
|
|
|
|
|
Exposure period [ms], up to 200 ms.
|
|
|
|
|
store_mode : str
|
|
|
|
|
Buffer operation mode
|
|
|
|
|
*'Recorder' to record in buffer
|
|
|
|
|
*'FIFO buffer' for continous streaming
|
|
|
|
|
data_format : str
|
|
|
|
|
Usually set to 'ZEROMQ'
|
|
|
|
|
"""
|
|
|
|
|
if self.state not in ("IDLE"):
|
|
|
|
|
raise RuntimeError(f"Can't change configuration from state {self.state}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# If Bluesky style configure
|
|
|
|
|
if d is not None:
|
|
|
|
|
# Commonly changed settings
|
|
|
|
|
if 'num_images' in d:
|
|
|
|
|
self.file_savestop.set(d['num_images']).wait()
|
|
|
|
|
if 'exposure_time_ms' in d:
|
|
|
|
|
self.acquire_time.set(d['exposure_time_ms']).wait()
|
|
|
|
|
if 'exposure_period_ms' in d:
|
|
|
|
|
self.acquire_delay.set(d['exposure_period_ms']).wait()
|
|
|
|
|
if 'exposure_period_ms' in d:
|
|
|
|
|
self.acquire_delay.set(d['exposure_period_ms']).wait()
|
|
|
|
|
if 'store_mode' in d:
|
|
|
|
|
self.bufferStoreMode.set(d['store_mode']).wait()
|
|
|
|
|
if 'data_format' in d:
|
|
|
|
|
self.file_format.set(d['data_format']).wait()
|
|
|
|
|
if "exposure_num_burst" in d:
|
|
|
|
|
self.file_savestop.set(d["exposure_num_burst"]).wait()
|
|
|
|
|
if "exposure_time_ms" in d:
|
|
|
|
|
self.acquire_time.set(d["exposure_time_ms"]).wait()
|
|
|
|
|
if "exposure_period_ms" in d:
|
|
|
|
|
self.acquire_delay.set(d["exposure_period_ms"]).wait()
|
|
|
|
|
if "exposure_period_ms" in d:
|
|
|
|
|
self.acquire_delay.set(d["exposure_period_ms"]).wait()
|
|
|
|
|
if "store_mode" in d:
|
|
|
|
|
self.bufferStoreMode.set(d["store_mode"]).wait()
|
|
|
|
|
if "data_format" in d:
|
|
|
|
|
self.file_format.set(d["data_format"]).wait()
|
|
|
|
|
|
|
|
|
|
# State machine
|
|
|
|
|
# Initial: BUSY and SET both low
|
|
|
|
|
# 0. Write 1 to SET_PARAM
|
|
|
|
|
# 1. BUSY goes high, SET stays low
|
|
|
|
|
# 0. Write 1 to SET_PARAM
|
|
|
|
|
# 1. BUSY goes high, SET stays low
|
|
|
|
|
# 2. BUSY goes low, SET goes high
|
|
|
|
|
# 3. BUSY stays low, SET goes low
|
|
|
|
|
# So we need a 'negedge' on SET_PARAM
|
|
|
|
|
self.camSetParam.set(1).wait()
|
|
|
|
|
|
|
|
|
|
def fallingEdge(*args, old_value, value, timestamp, **kwargs):
|
|
|
|
|
return bool(old_value and not value)
|
|
|
|
|
|
|
|
|
|
# Subscribe and wait for update
|
|
|
|
|
status = SubscriptionStatus(self.camSetParam, fallingEdge, timeout=5, settle_time=0.5)
|
|
|
|
|
status.wait()
|
|
|
|
|
|
|
|
|
|
def bluestage(self):
|
|
|
|
|
"""Bluesky style stage: arm the detector
|
|
|
|
|
"""
|
|
|
|
|
"""Bluesky style stage: arm the detector"""
|
|
|
|
|
logger.warning("Staging PCO")
|
|
|
|
|
# Acquisition is only allowed when the IOC is not busy
|
|
|
|
|
if self.state in ("OFFLINE", "BUSY", "REMOVED", "RUNNING"):
|
|
|
|
|
raise RuntimeError(f"Camera in in state: {self.state}")
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
self.bufferStoreMode.get() in ("Recorder", 0)
|
|
|
|
|
and self.file_savestop.get() > self.buffer_size.get()
|
|
|
|
|
):
|
|
|
|
|
self.logger.warning(
|
|
|
|
|
"You're about to send some empty images, {self.file_savestop.get()} is above buffer size"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Start the acquisition (this sets parameers and starts acquisition)
|
|
|
|
|
self.camStatusCmd.set("Running").wait()
|
|
|
|
|
|
|
|
|
|
# Subscribe and wait for update
|
|
|
|
|
def isRunning(*args, old_value, value, timestamp, **kwargs):
|
|
|
|
|
return bool(value==6)
|
|
|
|
|
return bool(value == 6)
|
|
|
|
|
|
|
|
|
|
status = SubscriptionStatus(self.camStatusCode, isRunning, timeout=5, settle_time=0.2)
|
|
|
|
|
status.wait()
|
|
|
|
|
|
|
|
|
|
def blueunstage(self):
|
|
|
|
|
"""Bluesky style unstage: stop the detector
|
|
|
|
|
"""
|
|
|
|
|
"""Bluesky style unstage: stop the detector"""
|
|
|
|
|
self.camStatusCmd.set("Idle").wait()
|
|
|
|
|
self.custom_prepare.stop_monitor = True
|
|
|
|
|
|
|
|
|
|
# Subscribe and wait for update
|
|
|
|
|
def isIdle(*args, old_value, value, timestamp, **kwargs):
|
|
|
|
|
return bool(value==2)
|
|
|
|
|
status = SubscriptionStatus(self.camStatusCode, isIdle, timeout=5, settle_time=0.2)
|
|
|
|
|
status.wait()
|
|
|
|
|
|
|
|
|
|
# Data streaming is stopped by setting the max index to 0
|
|
|
|
|
# FIXME: This might interrupt data transfer
|
|
|
|
|
self.file_savestop.set(0).wait()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bluekickoff(self):
|
|
|
|
|
""" Start data transfer
|
|
|
|
|
|
|
|
|
|
"""Start data transfer
|
|
|
|
|
|
|
|
|
|
TODO: Need to revisit this once triggering is complete
|
|
|
|
|
"""
|
|
|
|
|
self.file_transfer.set(1).wait()
|
|
|
|
|
|
|
|
|
|
# def complete(self):
|
|
|
|
|
# """ Wait until the images have been sent"""
|
|
|
|
|
# def areWeSending(*args, value, timestamp, **kwargs):
|
|
|
|
|
# return not bool(value)
|
|
|
|
|
# status = SubscriptionStatus(self.file_savebusy, haveWeSentIt, timeout=None, settle_time=0.2)
|
|
|
|
|
# return status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PcoEdge5M(HelgeCameraBase):
|
|
|
|
|
"""Ophyd baseclass for PCO.Edge cameras
|
|
|
|
|
|
|
|
|
|
This class provides wrappers for Helge's camera IOCs around SwissFEL and
|
|
|
|
|
for high performance SLS 2.0 cameras. Theese are mostly PCO cameras running
|
|
|
|
|
|
|
|
|
|
This class provides wrappers for Helge's camera IOCs around SwissFEL and
|
|
|
|
|
for high performance SLS 2.0 cameras. Theese are mostly PCO cameras running
|
|
|
|
|
on a special Windows IOC host with lots of RAM and CPU power.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
@@ -310,28 +398,36 @@ class PcoEdge5M(HelgeCameraBase):
|
|
|
|
|
camRate = Component(EpicsSignalRO, "CAMRATE", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
|
|
|
|
|
# ########################################################################
|
|
|
|
|
# Acquisition configuration
|
|
|
|
|
# Acquisition configuration
|
|
|
|
|
acqMode = Component(EpicsSignalRO, "ACQMODE", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
acqDelay = Component(EpicsSignalRO, "DELAY", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
acqTriggerEna = Component(EpicsSignalRO, "TRIGGER", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
#acqTriggerSource = Component(EpicsSignalRO, "TRIGGERSOURCE", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
#acqTriggerEdge = Component(EpicsSignalRO, "TRIGGEREDGE", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
|
|
|
|
|
# acqTriggerSource = Component(EpicsSignalRO, "TRIGGERSOURCE", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
# acqTriggerEdge = Component(EpicsSignalRO, "TRIGGEREDGE", auto_monitor=True, kind=Kind.config)
|
|
|
|
|
|
|
|
|
|
# ########################################################################
|
|
|
|
|
# Image size settings
|
|
|
|
|
# Priority is: binning -> roi -> final size
|
|
|
|
|
pxRoiX_lo = Component(EpicsSignal, "REGIONX_START", put_complete=True, auto_monitor=True, kind=Kind.config)
|
|
|
|
|
pxRoiX_hi = Component(EpicsSignal, "REGIONX_END", put_complete=True, auto_monitor=True, kind=Kind.config)
|
|
|
|
|
pxRoiY_lo = Component(EpicsSignal, "REGIONY_START", put_complete=True, auto_monitor=True, kind=Kind.config)
|
|
|
|
|
pxRoiY_hi = Component(EpicsSignal, "REGIONY_END", put_complete=True, auto_monitor=True, kind=Kind.config)
|
|
|
|
|
pxRoiX_lo = Component(
|
|
|
|
|
EpicsSignal, "REGIONX_START", put_complete=True, auto_monitor=True, kind=Kind.config
|
|
|
|
|
)
|
|
|
|
|
pxRoiX_hi = Component(
|
|
|
|
|
EpicsSignal, "REGIONX_END", put_complete=True, auto_monitor=True, kind=Kind.config
|
|
|
|
|
)
|
|
|
|
|
pxRoiY_lo = Component(
|
|
|
|
|
EpicsSignal, "REGIONY_START", put_complete=True, auto_monitor=True, kind=Kind.config
|
|
|
|
|
)
|
|
|
|
|
pxRoiY_hi = Component(
|
|
|
|
|
EpicsSignal, "REGIONY_END", put_complete=True, auto_monitor=True, kind=Kind.config
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def configure(self, d: dict = {}) -> tuple:
|
|
|
|
|
"""
|
|
|
|
|
Camera configuration instructions:
|
|
|
|
|
After setting the corresponding PVs, one needs to process SET_PARAM and wait until
|
|
|
|
|
BUSY_SET_PARAM goes high and low, followed by SET_PARAM goes high and low. This will
|
|
|
|
|
both send the settings to the camera and allocate the necessary buffers in the correct
|
|
|
|
|
size and shape (that takes time). Starting the exposure with CAMERASTATUS will also
|
|
|
|
|
After setting the corresponding PVs, one needs to process SET_PARAM and wait until
|
|
|
|
|
BUSY_SET_PARAM goes high and low, followed by SET_PARAM goes high and low. This will
|
|
|
|
|
both send the settings to the camera and allocate the necessary buffers in the correct
|
|
|
|
|
size and shape (that takes time). Starting the exposure with CAMERASTATUS will also
|
|
|
|
|
call SET_PARAM, but it might take long.
|
|
|
|
|
|
|
|
|
|
NOTE:
|
|
|
|
|
@@ -360,18 +456,18 @@ class PcoEdge5M(HelgeCameraBase):
|
|
|
|
|
# Need to be smart how we set the ROI....
|
|
|
|
|
# Image sensor is 2560x2160 (X and Y)
|
|
|
|
|
# Values are rounded to multiples of 16
|
|
|
|
|
if 'image_width' in d and d['image_width'] is not None:
|
|
|
|
|
width = d['image_width']
|
|
|
|
|
self.pxRoiX_lo.set(2560/2-width/2).wait()
|
|
|
|
|
self.pxRoiX_hi.set(2560/2+width/2).wait()
|
|
|
|
|
if 'image_height' in d and d['image_height'] is not None:
|
|
|
|
|
height = d['image_height']
|
|
|
|
|
self.pxRoiY_lo.set(2160/2-height/2).wait()
|
|
|
|
|
self.pxRoiY_hi.set(2160/2+height/2).wait()
|
|
|
|
|
if 'image_binx' in d and d['image_binx'] is not None:
|
|
|
|
|
self.pxBinX.set(d['image_binx']).wait()
|
|
|
|
|
if 'image_biny' in d and d['image_biny'] is not None:
|
|
|
|
|
self.pxBinY.set(d['image_biny']).wait()
|
|
|
|
|
if "image_width" in d and d["image_width"] is not None:
|
|
|
|
|
width = d["image_width"]
|
|
|
|
|
self.pxRoiX_lo.set(2560 / 2 - width / 2).wait()
|
|
|
|
|
self.pxRoiX_hi.set(2560 / 2 + width / 2).wait()
|
|
|
|
|
if "image_height" in d and d["image_height"] is not None:
|
|
|
|
|
height = d["image_height"]
|
|
|
|
|
self.pxRoiY_lo.set(2160 / 2 - height / 2).wait()
|
|
|
|
|
self.pxRoiY_hi.set(2160 / 2 + height / 2).wait()
|
|
|
|
|
if "image_binx" in d and d["image_binx"] is not None:
|
|
|
|
|
self.pxBinX.set(d["image_binx"]).wait()
|
|
|
|
|
if "image_biny" in d and d["image_biny"] is not None:
|
|
|
|
|
self.pxBinY.set(d["image_biny"]).wait()
|
|
|
|
|
|
|
|
|
|
# Call super() to commit the changes
|
|
|
|
|
super().configure(d)
|
|
|
|
|
@@ -379,7 +475,7 @@ class PcoEdge5M(HelgeCameraBase):
|
|
|
|
|
|
|
|
|
|
# Automatically connect to test camera if directly invoked
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Drive data collection
|
|
|
|
|
cam = PcoEdgeBase("X02DA-CCDCAM2:", name="mcpcam")
|
|
|
|
|
cam.wait_for_connection()
|
|
|
|
|
|