PCO step scanning works

This commit is contained in:
gac-x05la
2025-02-13 18:16:42 +01:00
parent aea898e60f
commit 163ef3c7a5
7 changed files with 358 additions and 171 deletions

View File

@@ -188,12 +188,13 @@ pcocam:
prefix: 'X02DA-CCDCAM2:' prefix: 'X02DA-CCDCAM2:'
deviceTags: deviceTags:
- camera - camera
- trigger
- pcocam - pcocam
enabled: true enabled: true
onFailure: buffer onFailure: buffer
readOnly: false readOnly: false
readoutPriority: monitored readoutPriority: monitored
softwareTrigger: false softwareTrigger: true
pcodaq: pcodaq:
description: GigaFrost stdDAQ client description: GigaFrost stdDAQ client

View File

@@ -6,16 +6,20 @@ Created on Wed Dec 6 11:33:54 2023
""" """
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind
from ophyd.status import SubscriptionStatus from ophyd.status import SubscriptionStatus, DeviceStatus
import time 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 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: try:
from bec_lib import bec_logger from bec_lib import bec_logger
logger = bec_logger.logger logger = bec_logger.logger
except ModuleNotFoundError: except ModuleNotFoundError:
import logging import logging
logger = logging.getLogger("PcoEdgeCam") 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. This class will be called by the custom_prepare_cls attribute of the detector class.
""" """
def on_stage(self) -> None: 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 # PCO can finish a run without explicit unstaging
if self.parent.state not in ("IDLE"): 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() self.parent.unstage()
time.sleep(0.5) time.sleep(0.5)
# Fish out our configuration from scaninfo (via explicit or generic addressing) # Fish out our configuration from scaninfo (via explicit or generic addressing)
scanparam = self.parent.scaninfo.scan_msg.info scanparam = self.parent.scaninfo.scan_msg.info
d = {} d = {}
if 'kwargs' in scanparam: if "kwargs" in scanparam:
scanargs = scanparam['kwargs'] scanargs = scanparam["kwargs"]
if 'num_images_total' in scanargs and scanargs['num_images_total'] is not None: if "exp_burst" in scanargs and scanargs["exp_burst"] is not None:
d['images_total'] = scanargs['num_images_total'] d["exposure_num_burst"] = scanargs["exp_burst"]
if 'image_width' in scanargs and scanargs['image_width'] is not None: if "image_width" in scanargs and scanargs["image_width"] is not None:
d['image_width'] = scanargs['image_width'] d["image_width"] = scanargs["image_width"]
if 'image_height' in scanargs and scanargs['image_height'] is not None: if "image_height" in scanargs and scanargs["image_height"] is not None:
d['image_height'] = scanargs['image_height'] d["image_height"] = scanargs["image_height"]
if 'exp_time' in scanargs and scanargs['exp_time'] is not None: if "exp_time" in scanargs and scanargs["exp_time"] is not None:
d['exposure_time_ms'] = scanargs['exp_time'] d["exposure_time_ms"] = scanargs["exp_time"]
if 'exp_period' in scanargs and scanargs['exp_period'] is not None: if "exp_period" in scanargs and scanargs["exp_period"] is not None:
d['exposure_period_ms'] = scanargs['exp_period'] d["exposure_period_ms"] = scanargs["exp_period"]
# if 'exp_burst' in scanargs and scanargs['exp_burst'] is not None: # if 'exp_burst' in scanargs and scanargs['exp_burst'] is not None:
# d['exposure_num_burst'] = scanargs['exp_burst'] # d['exposure_num_burst'] = scanargs['exp_burst']
# if 'acq_mode' in scanargs and scanargs['acq_mode'] is not None: # if 'acq_mode' in scanargs and scanargs['acq_mode'] is not None:
# d['acq_mode'] = scanargs['acq_mode'] # d['acq_mode'] = scanargs['acq_mode']
# elif self.parent.scaninfo.scan_type == "step": # elif self.parent.scaninfo.scan_type == "step":
# d['acq_mode'] = "default" # d['acq_mode'] = "default"
if 'pco_store_mode' in scanargs and scanargs['pco_store_mode'] is not None: if "pco_store_mode" in scanargs and scanargs["pco_store_mode"] is not None:
d['store_mode'] = scanargs['pco_store_mode'] d["store_mode"] = scanargs["pco_store_mode"]
if 'pco_data_format' in scanargs and scanargs['pco_data_format'] is not None: if "pco_data_format" in scanargs and scanargs["pco_data_format"] is not None:
d['data_format'] = scanargs['pco_data_format'] d["data_format"] = scanargs["pco_data_format"]
# Perform bluesky-style configuration # Perform bluesky-style configuration
if len(d) > 0: if len(d) > 0:
@@ -70,29 +75,86 @@ class PcoEdgeCameraMixin(CustomDeviceMixin):
self.parent.bluestage() self.parent.bluestage()
def on_unstage(self) -> None: def on_unstage(self) -> None:
"""Disarm the PCO.Edge camera """Disarm the PCO.Edge camera"""
"""
self.parent.blueunstage() self.parent.blueunstage()
def on_stop(self) -> None: def on_stop(self) -> None:
"""Stop the PCO.Edge camera """Stop the PCO.Edge camera"""
"""
self.parent.blueunstage() 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): class HelgeCameraBase(PSIDeviceBase):
"""Ophyd baseclass for Helge camera IOCs """Ophyd baseclass for Helge camera IOCs
This class provides wrappers for Helge's camera IOCs around SwissFEL and 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 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. only covers the absolute basics.
Probably the most important part is the configuration state machine. As Probably the most important part is the configuration state machine. As
the SET_PARAMS takes care of buffer allocations it might take some time, 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 as well as a full re-configuration is required every time we change the
binning, roi, etc... This is automatically performed upon starting an binning, roi, etc... This is automatically performed upon starting an
exposure (if it heven't been done before). exposure (if it heven't been done before).
The status flag state machine during re-configuration is: 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 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 Note that in FIFO mode buffer reads are destructive, to prevent this, we don't have EPICS preview
""" """
# ######################################################################## # ########################################################################
# General hardware info (in AD nomenclature) # General hardware info (in AD nomenclature)
manufacturer = Component(EpicsSignalRO, "QUERY", kind=Kind.config, doc="Camera model info") 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 # Acquisition commands
@@ -125,30 +187,41 @@ class HelgeCameraBase(PSIDeviceBase):
# ######################################################################## # ########################################################################
# Acquisition configuration (in AD nomenclature) # Acquisition configuration (in AD nomenclature)
acquire_time = Component(EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config) acquire_time = Component(
acquire_delay = Component(EpicsSignal, "DELAY", put_complete=True, auto_monitor=True, kind=Kind.config) EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config
trigger_mode = Component(EpicsSignal, "TRIGGER", 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) # Image size configuration (in AD nomenclature)
bin_x = Component(EpicsSignal, "BINX", put_complete=True, auto_monitor=True, kind=Kind.config) 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) 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_x = Component(
array_size_y = Component(EpicsSignalRO, "HEIGHT", auto_monitor=True, kind=Kind.config, doc="Final image height") 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 # General hardware info
camError = Component(EpicsSignalRO, "ERRCODE", auto_monitor=True, kind=Kind.config) 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) bufferRecMode = Component(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
bufferStoreMode = Component(EpicsSignal, "STOREMODE", 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) fileRecMode = Component(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
buffer_used = Component(EpicsSignalRO, "PIC_BUFFER", auto_monitor=True, kind=Kind.normal) 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_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 # File saving interface
@@ -158,15 +231,18 @@ class HelgeCameraBase(PSIDeviceBase):
file_savestop = Component(EpicsSignal, "SAVESTOP", put_complete=True, kind=Kind.config) file_savestop = Component(EpicsSignal, "SAVESTOP", put_complete=True, kind=Kind.config)
file_format = Component(EpicsSignal, "FILEFORMAT", 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_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 # Configuration state maschine with separate transition states
camStatusCode = Component(EpicsSignalRO, "STATUSCODE", auto_monitor=True, kind=Kind.config) camStatusCode = Component(EpicsSignalRO, "STATUSCODE", auto_monitor=True, kind=Kind.config)
camSetParam = Component(EpicsSignal, "SET_PARAM", 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) camCamera = Component(EpicsSignalRO, "CAMERA", auto_monitor=True, kind=Kind.config)
camCameraBusy = Component(EpicsSignalRO, "BUSY_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) camInitBusy = Component(EpicsSignalRO, "BUSY_INIT", auto_monitor=True, kind=Kind.config)
# ######################################################################## # ########################################################################
@@ -175,22 +251,24 @@ class HelgeCameraBase(PSIDeviceBase):
# ######################################################################## # ########################################################################
# Misc PVs # Misc PVs
#camRemoval = Component(EpicsSignalRO, "REMOVAL", 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) camStateString = Component(
EpicsSignalRO, "SS_CAMERA", string=True, auto_monitor=True, kind=Kind.config
)
@property @property
def state(self) -> str: def state(self) -> str:
""" Single word camera state""" """Single word camera state"""
if self.camSetParamBusy.value: if self.camSetParamBusy.value:
return "BUSY" return "BUSY"
if self.camStatusCode.value==2 and self.camInit.value==1: if self.camStatusCode.value == 2 and self.camInit.value == 1:
return "IDLE" return "IDLE"
if self.camStatusCode.value==6 and self.camInit.value==1: if self.camStatusCode.value == 6 and self.camInit.value == 1:
return "RUNNING" return "RUNNING"
#if self.camRemoval.value==0 and self.camInit.value==0: # if self.camRemoval.value==0 and self.camInit.value==0:
if self.camInit.value==0: if self.camInit.value == 0:
return "OFFLINE" return "OFFLINE"
#if self.camRemoval.value: # if self.camRemoval.value:
# return "REMOVED" # return "REMOVED"
return "UNKNOWN" return "UNKNOWN"
@@ -199,103 +277,113 @@ class HelgeCameraBase(PSIDeviceBase):
raise ReadOnlyError("State is a ReadOnly property") raise ReadOnlyError("State is a ReadOnly property")
def configure(self, d: dict = {}) -> tuple: def configure(self, d: dict = {}) -> tuple:
""" Configure the base Helge camera device """Configure the base Helge camera device
Parameters as 'd' dictionary Parameters as 'd' dictionary
---------------------------- ----------------------------
num_images : int num_images : int
Number of images to be taken during each scan. Meaning depends on Number of images to be taken during each scan. Meaning depends on
store mode. store mode.
exposure_time_ms : float exposure_time_ms : float
Exposure time [ms], usually gets set back to 20 ms Exposure time [ms], usually gets set back to 20 ms
exposure_period_ms : float exposure_period_ms : float
Exposure period [ms], up to 200 ms. Exposure period [ms], up to 200 ms.
store_mode : str store_mode : str
Buffer operation mode Buffer operation mode
*'Recorder' to record in buffer *'Recorder' to record in buffer
*'FIFO buffer' for continous streaming *'FIFO buffer' for continous streaming
data_format : str data_format : str
Usually set to 'ZEROMQ' Usually set to 'ZEROMQ'
""" """
if self.state not in ("IDLE"): if self.state not in ("IDLE"):
raise RuntimeError(f"Can't change configuration from state {self.state}") raise RuntimeError(f"Can't change configuration from state {self.state}")
# If Bluesky style configure # If Bluesky style configure
if d is not None: if d is not None:
# Commonly changed settings # Commonly changed settings
if 'num_images' in d: if "exposure_num_burst" in d:
self.file_savestop.set(d['num_images']).wait() self.file_savestop.set(d["exposure_num_burst"]).wait()
if 'exposure_time_ms' in d: if "exposure_time_ms" in d:
self.acquire_time.set(d['exposure_time_ms']).wait() self.acquire_time.set(d["exposure_time_ms"]).wait()
if 'exposure_period_ms' in d: if "exposure_period_ms" in d:
self.acquire_delay.set(d['exposure_period_ms']).wait() self.acquire_delay.set(d["exposure_period_ms"]).wait()
if 'exposure_period_ms' in d: if "exposure_period_ms" in d:
self.acquire_delay.set(d['exposure_period_ms']).wait() self.acquire_delay.set(d["exposure_period_ms"]).wait()
if 'store_mode' in d: if "store_mode" in d:
self.bufferStoreMode.set(d['store_mode']).wait() self.bufferStoreMode.set(d["store_mode"]).wait()
if 'data_format' in d: if "data_format" in d:
self.file_format.set(d['data_format']).wait() self.file_format.set(d["data_format"]).wait()
# State machine # State machine
# Initial: BUSY and SET both low # Initial: BUSY and SET both low
# 0. Write 1 to SET_PARAM # 0. Write 1 to SET_PARAM
# 1. BUSY goes high, SET stays low # 1. BUSY goes high, SET stays low
# 2. BUSY goes low, SET goes high # 2. BUSY goes low, SET goes high
# 3. BUSY stays low, SET goes low # 3. BUSY stays low, SET goes low
# So we need a 'negedge' on SET_PARAM # So we need a 'negedge' on SET_PARAM
self.camSetParam.set(1).wait() self.camSetParam.set(1).wait()
def fallingEdge(*args, old_value, value, timestamp, **kwargs): def fallingEdge(*args, old_value, value, timestamp, **kwargs):
return bool(old_value and not value) return bool(old_value and not value)
# Subscribe and wait for update # Subscribe and wait for update
status = SubscriptionStatus(self.camSetParam, fallingEdge, timeout=5, settle_time=0.5) status = SubscriptionStatus(self.camSetParam, fallingEdge, timeout=5, settle_time=0.5)
status.wait() status.wait()
def bluestage(self): 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 # Acquisition is only allowed when the IOC is not busy
if self.state in ("OFFLINE", "BUSY", "REMOVED", "RUNNING"): if self.state in ("OFFLINE", "BUSY", "REMOVED", "RUNNING"):
raise RuntimeError(f"Camera in in state: {self.state}") 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) # Start the acquisition (this sets parameers and starts acquisition)
self.camStatusCmd.set("Running").wait() self.camStatusCmd.set("Running").wait()
# Subscribe and wait for update # Subscribe and wait for update
def isRunning(*args, old_value, value, timestamp, **kwargs): 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 = SubscriptionStatus(self.camStatusCode, isRunning, timeout=5, settle_time=0.2)
status.wait() status.wait()
def blueunstage(self): def blueunstage(self):
"""Bluesky style unstage: stop the detector """Bluesky style unstage: stop the detector"""
"""
self.camStatusCmd.set("Idle").wait() self.camStatusCmd.set("Idle").wait()
self.custom_prepare.stop_monitor = True 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 # Data streaming is stopped by setting the max index to 0
# FIXME: This might interrupt data transfer
self.file_savestop.set(0).wait() self.file_savestop.set(0).wait()
def bluekickoff(self): def bluekickoff(self):
""" Start data transfer """Start data transfer
TODO: Need to revisit this once triggering is complete TODO: Need to revisit this once triggering is complete
""" """
self.file_transfer.set(1).wait() 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): class PcoEdge5M(HelgeCameraBase):
"""Ophyd baseclass for PCO.Edge cameras """Ophyd baseclass for PCO.Edge cameras
This class provides wrappers for Helge's camera IOCs around SwissFEL and 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 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. 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) camRate = Component(EpicsSignalRO, "CAMRATE", auto_monitor=True, kind=Kind.config)
# ######################################################################## # ########################################################################
# Acquisition configuration # Acquisition configuration
acqMode = Component(EpicsSignalRO, "ACQMODE", auto_monitor=True, kind=Kind.config) acqMode = Component(EpicsSignalRO, "ACQMODE", auto_monitor=True, kind=Kind.config)
acqDelay = Component(EpicsSignalRO, "DELAY", 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) acqTriggerEna = Component(EpicsSignalRO, "TRIGGER", auto_monitor=True, kind=Kind.config)
#acqTriggerSource = Component(EpicsSignalRO, "TRIGGERSOURCE", 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) # acqTriggerEdge = Component(EpicsSignalRO, "TRIGGEREDGE", auto_monitor=True, kind=Kind.config)
# ######################################################################## # ########################################################################
# Image size settings # Image size settings
# Priority is: binning -> roi -> final size # Priority is: binning -> roi -> final size
pxRoiX_lo = Component(EpicsSignal, "REGIONX_START", put_complete=True, auto_monitor=True, kind=Kind.config) pxRoiX_lo = Component(
pxRoiX_hi = Component(EpicsSignal, "REGIONX_END", put_complete=True, auto_monitor=True, kind=Kind.config) EpicsSignal, "REGIONX_START", 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_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: def configure(self, d: dict = {}) -> tuple:
""" """
Camera configuration instructions: Camera configuration instructions:
After setting the corresponding PVs, one needs to process SET_PARAM and wait until 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 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 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 size and shape (that takes time). Starting the exposure with CAMERASTATUS will also
call SET_PARAM, but it might take long. call SET_PARAM, but it might take long.
NOTE: NOTE:
@@ -360,18 +456,18 @@ class PcoEdge5M(HelgeCameraBase):
# Need to be smart how we set the ROI.... # Need to be smart how we set the ROI....
# Image sensor is 2560x2160 (X and Y) # Image sensor is 2560x2160 (X and Y)
# Values are rounded to multiples of 16 # Values are rounded to multiples of 16
if 'image_width' in d and d['image_width'] is not None: if "image_width" in d and d["image_width"] is not None:
width = d['image_width'] width = d["image_width"]
self.pxRoiX_lo.set(2560/2-width/2).wait() self.pxRoiX_lo.set(2560 / 2 - width / 2).wait()
self.pxRoiX_hi.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: if "image_height" in d and d["image_height"] is not None:
height = d['image_height'] height = d["image_height"]
self.pxRoiY_lo.set(2160/2-height/2).wait() self.pxRoiY_lo.set(2160 / 2 - height / 2).wait()
self.pxRoiY_hi.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: if "image_binx" in d and d["image_binx"] is not None:
self.pxBinX.set(d['image_binx']).wait() self.pxBinX.set(d["image_binx"]).wait()
if 'image_biny' in d and d['image_biny'] is not None: if "image_biny" in d and d["image_biny"] is not None:
self.pxBinY.set(d['image_biny']).wait() self.pxBinY.set(d["image_biny"]).wait()
# Call super() to commit the changes # Call super() to commit the changes
super().configure(d) super().configure(d)
@@ -379,7 +475,7 @@ class PcoEdge5M(HelgeCameraBase):
# Automatically connect to test camera if directly invoked # Automatically connect to test camera if directly invoked
if __name__ == "__main__": if __name__ == "__main__":
# Drive data collection # Drive data collection
cam = PcoEdgeBase("X02DA-CCDCAM2:", name="mcpcam") cam = PcoEdgeBase("X02DA-CCDCAM2:", name="mcpcam")
cam.wait_for_connection() cam.wait_for_connection()

View File

@@ -119,7 +119,7 @@ class StdDaqMixin(CustomDeviceMixin):
for msg in self.parent._wsclient: for msg in self.parent._wsclient:
message = json.loads(msg) message = json.loads(msg)
self.parent.runstatus.put(message["status"], force=True) self.parent.runstatus.put(message["status"], force=True)
logger.info(f"[{self.parent.name}] Pushed status: {message['status']}") # logger.info(f"[{self.parent.name}] Pushed status: {message['status']}")
except (ConnectionClosedError, ConnectionClosedOK, AssertionError): except (ConnectionClosedError, ConnectionClosedOK, AssertionError):
# Libraty throws theese after connection is closed # Libraty throws theese after connection is closed
return return

View File

@@ -97,7 +97,7 @@ class TomcatStepScan(ScanBase):
yield from self._move_scan_motors_and_wait(pos) yield from self._move_scan_motors_and_wait(pos)
time.sleep(self.settling_time) time.sleep(self.settling_time)
trigger_time = self.exp_time * self.burst_at_each_point trigger_time = 0.001*self.exp_time * self.burst_at_each_point
yield from self.stubs.trigger(min_wait=trigger_time) yield from self.stubs.trigger(min_wait=trigger_time)
# yield from self.stubs.trigger(group='trigger', point_id=self.point_id) # yield from self.stubs.trigger(group='trigger', point_id=self.point_id)
# time.sleep(trigger_time) # time.sleep(trigger_time)

View File

@@ -46,7 +46,7 @@ class AcquireDark(Acquire):
def scan_core(self): def scan_core(self):
# close the shutter # close the shutter
# yield from self._move_scan_motors_and_wait(self.dark_shutter_pos) yield from self._move_scan_motors_and_wait(self.dark_shutter_pos)
#yield from self.stubs.set_and_wait(device=[self.shutter], positions=[0]) #yield from self.stubs.set_and_wait(device=[self.shutter], positions=[0])
yield from super().scan_core() yield from super().scan_core()

View File

@@ -18,6 +18,25 @@ def dev_disable_all():
dev[k].enabled = False dev[k].enabled = False
def cam_select(camera: str):
"""Select the active camera pipeline"""
if isinstance(camera, str):
if camera in ("gf", "gf2", "gigafrost"):
dev.gfcam.enabled = True
dev.gfdaq.enabled = True
dev.daq_stream0.enabled = True
dev.pcocam.enabled = False
dev.pcodaq.enabled = False
dev.pco_stream0.enabled = False
if camera in ("pco", "pco.edge", "pcoedge"):
dev.gfcam.enabled = False
dev.gfdaq.enabled = False
dev.daq_stream0.enabled = False
dev.pcocam.enabled = True
dev.pcodaq.enabled = True
dev.pco_stream0.enabled = True
def anotherstepscan( def anotherstepscan(
scan_start, scan_start,
@@ -26,8 +45,9 @@ def anotherstepscan(
exp_time=0.005, exp_time=0.005,
exp_burst=5, exp_burst=5,
settling_time=0, settling_time=0,
image_width=2016, camera=None,
image_height=2016, image_width=None,
image_height=None,
sync="inp1", sync="inp1",
**kwargs **kwargs
): ):
@@ -41,31 +61,49 @@ def anotherstepscan(
-------- --------
demostepscan(scan_start=-32, scan_end=148, steps=180, exp_time=0.005, exp_burst=5) demostepscan(scan_start=-32, scan_end=148, steps=180, exp_time=0.005, exp_burst=5)
""" """
# if not bl_check_beam(): # Check beamline status before scan
# raise RuntimeError("Beamline is not in ready state") if not bl_check_beam():
raise RuntimeError("Beamline is not in ready state")
# Enable the correct camera before scan
cam_select(camera)
# This scan uses software triggering
if isinstance(camera, str):
if camera in ("gf", "gf2", "gigafrost"):
dev.gfcam.software_trigger = True
if camera in ("pco", "pco.edge", "pcoedge"):
dev.pcocam.software_trigger = True
dev_disable_all() # Disable aerotech controller
# dev.es1_tasks.enabled = False
# dev.es1_psod.enabled = False
# dev.es1_ddaq.enabled = True
# Enable scan motor
dev.es1_roty.enabled = True dev.es1_roty.enabled = True
#dev.es1_ddaq.enabled = True
dev.gfcam.enabled = True
dev.gfdaq.enabled = True
dev.daq_stream0.enabled = True
dev.daq_stream1.enabled = False
print("Handing over to 'scans.tomcatstepscan'") print("Handing over to 'scans.tomcatstepscan'")
scans.tomcatstepscan( try:
scan_start=scan_start, scans.tomcatstepscan(
scan_end=scan_end, scan_start=scan_start,
steps=steps, scan_end=scan_end,
exp_time=exp_time, steps=steps,
exp_burst=exp_burst, exp_time=exp_time,
relative=False, exp_burst=exp_burst,
image_width=image_width, relative=False,
image_height=image_height, image_width=image_width,
settling_time=settling_time, image_height=image_height,
sync=sync, settling_time=settling_time,
**kwargs sync=sync,
) **kwargs
)
finally:
# if isinstance(camera, str):
# if camera in ("gf", "gf2", "gigafrost"):
# dev.gfcam.software_trigger = False
# if camera in ("pco", "pco.edge", "pcoedge"):
# dev.pcocam.software_trigger = False
pass
@@ -78,8 +116,9 @@ def anothersequencescan(
repmode="PosNeg", repmode="PosNeg",
exp_time=0.005, exp_time=0.005,
exp_burst=180, exp_burst=180,
image_width=2016, camera=None,
image_height=2016, image_width=None,
image_height=None,
sync="pso", sync="pso",
**kwargs **kwargs
): ):
@@ -96,16 +135,15 @@ def anothersequencescan(
-------- --------
>>> anothersequencescan(33, 180, 180, exp_time=0.005, exp_frames=1800, repeats=10) >>> anothersequencescan(33, 180, 180, exp_time=0.005, exp_frames=1800, repeats=10)
""" """
# Check beamline status before scan
if not bl_check_beam(): if not bl_check_beam():
raise RuntimeError("Beamline is not in ready state") raise RuntimeError("Beamline is not in ready state")
# Enable the correct camera before scan
cam_select(camera)
# Enabling aerotech controller
dev.es1_tasks.enabled = True dev.es1_tasks.enabled = True
dev.es1_psod.enabled = False dev.es1_psod.enabled = False
dev.es1_ddaq.enabled = True dev.es1_ddaq.enabled = True
dev.gfcam.enabled = True
dev.gfdaq.enabled = True
dev.daq_stream0.enabled = True
dev.daq_stream1.enabled = False
print("Handing over to 'scans.sequencescan'") print("Handing over to 'scans.sequencescan'")
scans.tomcatsimplesequencescan( scans.tomcatsimplesequencescan(
@@ -131,8 +169,9 @@ def anothersnapnstepscan(
steps, steps,
exp_time=0.005, exp_time=0.005,
exp_burst=180, exp_burst=180,
image_width=2016, camera=None,
image_height=2016, image_width=None,
image_height=None,
settling_time=0.1, settling_time=0.1,
sync="pso", sync="pso",
**kwargs **kwargs
@@ -148,17 +187,15 @@ def anothersnapnstepscan(
-------- --------
>>> anothersnapnstepscan(33, 180, 180, exp_time=0.005, exp_frames=1800, repeats=10) >>> anothersnapnstepscan(33, 180, 180, exp_time=0.005, exp_frames=1800, repeats=10)
""" """
# Check beamline status before scan
if not bl_check_beam(): if not bl_check_beam():
raise RuntimeError("Beamline is not in ready state") raise RuntimeError("Beamline is not in ready state")
# Enable the correct camera before scan
cam_select(camera)
# Enabling aerotech controller
dev.es1_tasks.enabled = True dev.es1_tasks.enabled = True
dev.es1_psod.enabled = False dev.es1_psod.enabled = False
dev.es1_ddaq.enabled = True dev.es1_ddaq.enabled = True
dev.gfcam.enabled = True
dev.gfdaq.enabled = True
dev.daq_stream0.enabled = True
dev.daq_stream1.enabled = False
print("Handing over to 'scans.tomcatsnapnstepscan'") print("Handing over to 'scans.tomcatsnapnstepscan'")
scans.tomcatsnapnstepscan( scans.tomcatsnapnstepscan(
@@ -174,3 +211,58 @@ def anothersnapnstepscan(
**kwargs **kwargs
) )
def ascan(motor, scan_start, scan_end, steps, exp_time, datasource, visual=True, **kwargs):
"""Demo step scan with plotting
This is a simple user-space demo step scan with automatic plorring and fitting via the BEC.
It's mostly a standard BEC scan, just adds GUI setup and fits a hardcoded LlinearModel at
the end (other models are more picky on the initial parameters).
Example:
--------
ascan(dev.dccm_energy, 12,13, steps=21, exp_time=0.1, datasource=dev.dccm_xbpm)
"""
# Dummy method to check beamline status
if not bl_check_beam():
raise RuntimeError("Beamline is not in ready state")
if visual:
# Get or create scan specific window
window = None
for _, val in bec.gui.windows.items():
if val.title == "CurrentScan":
window = val.widget
window.clear_all()
if window is None:
window = bec.gui.new("CurrentScan")
# Draw a simploe plot in the window
dock = window.add_dock(f"ScanDisplay {motor}")
plt1 = dock.add_widget("BECWaveformWidget")
plt1.plot(x_name=motor, y_name=datasource)
plt1.set_x_label(motor)
plt1.set_y_label(datasource)
plt1.add_dap(motor, datasource, dap="LinearModel")
window.show()
print("Handing over to 'scans.line_scan'")
if "relative" in kwargs:
del kwargs["relative"]
s = scans.line_scan(
motor, scan_start, scan_end, steps=steps, exp_time=exp_time, relative=False, **kwargs
)
if visual:
fit = plt1.get_dap_params()
else:
fit = bec.dap.LinearModel.fit(s, motor.name, motor.name, datasource.name, datasource.name)
# TODO: Move to fitted value... like center, peak, edge, etc...
return s, fit

View File

@@ -4,9 +4,7 @@
def bl_check_beam(): def bl_check_beam():
"""Checks beamline status""" """Checks beamline status"""
motor_enabled = bool(dev.es1_roty.motor_enable.get()) return True
result = motor_enabled
return result
def demostepscan( def demostepscan(