GF seems done, working on PCO

This commit is contained in:
gac-x05la
2025-03-21 15:42:40 +01:00
parent 32b976f9d6
commit 045f348322
4 changed files with 459 additions and 359 deletions

View File

@@ -6,22 +6,17 @@ Created on Thu Jun 27 17:28:43 2024
@author: mohacsi_i
"""
from time import sleep
from time import sleep, time
import numpy as np
from bec_lib.logger import bec_logger
from ophyd import DeviceStatus
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from tomcat_bec.devices.gigafrost.gigafrost_base import GigaFrostBase
from tomcat_bec.devices.gigafrost.std_daq_client import (
StdDaqClient,
StdDaqStatus,
)
import tomcat_bec.devices.gigafrost.gfconstants as const
from tomcat_bec.devices.gigafrost.gigafrost_base import GigaFrostBase
from tomcat_bec.devices.gigafrost.std_daq_preview import StdDaqPreview
from tomcat_bec.devices.gigafrost.std_daq_client import StdDaqClient, StdDaqStatus
logger = bec_logger.logger
@@ -62,6 +57,7 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
# pylint: disable=too-many-instance-attributes
USER_ACCESS = [
"complete",
"exposure_mode",
"fix_nframes_mode",
"trigger_mode",
@@ -182,13 +178,13 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
if d and self.backend is not None:
daq_update = {}
if "image_height" in d:
daq_update['image_pixel_height'] = d["image_height"]
daq_update["image_pixel_height"] = d["image_height"]
if "image_width" in d:
daq_update['image_pixel_width'] = d["image_width"]
daq_update["image_pixel_width"] = d["image_width"]
if "bit_depth" in d:
daq_update['bit_depth'] = d["bit_depth"]
daq_update["bit_depth"] = d["bit_depth"]
if "number_of_writers" in d:
daq_update['number_of_writers'] = d["number_of_writers"]
daq_update["number_of_writers"] = d["number_of_writers"]
if daq_update:
self.backend.set_config(daq_update, force=False)
@@ -493,28 +489,28 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
self.backend.shutdown()
super().destroy()
def _on_preview_update(self, img:np.ndarray, header: dict):
def _on_preview_update(self, img: np.ndarray, header: dict):
"""Send preview stream and update frame index counter"""
self.num_images_counter.put(header['frame'], force=True)
self.num_images_counter.put(header["frame"], force=True)
self._run_subs(sub_type=self.SUB_DEVICE_MONITOR_2D, obj=self, value=img)
# def acq_done(self) -> DeviceStatus:
# """
# Check if the acquisition is done. For the GigaFrost camera, this is
# done by checking the status of the backend as the camera does not
# provide any feedback about its internal state.
def acq_done(self) -> DeviceStatus:
"""
Check if the acquisition is done. For the GigaFrost camera, this is
done by checking the status of the backend as the camera does not
provide any feedback about its internal state.
# Returns:
# DeviceStatus: The status of the acquisition
# """
# status = DeviceStatus(self)
# if self.backend is not None:
# self.backend.add_status_callback(
# status,
# success=[StdDaqStatus.IDLE, StdDaqStatus.FILE_SAVED],
# error=[StdDaqStatus.REJECTED, StdDaqStatus.ERROR],
# )
# return status
Returns:
DeviceStatus: The status of the acquisition
"""
status = DeviceStatus(self)
if self.backend is not None:
self.backend.add_status_callback(
status,
success=[StdDaqStatus.IDLE, StdDaqStatus.FILE_SAVED],
error=[StdDaqStatus.REJECTED, StdDaqStatus.ERROR],
)
return status
########################################
# Beamline Specific Implementations #
@@ -590,11 +586,11 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
)
self.num_images.set(num_points).wait()
if "daq_file_path" in scan_args and scan_args["daq_file_path"] is not None:
self.file_path.set(scan_args['daq_file_path']).wait()
self.file_path.set(scan_args["daq_file_path"]).wait()
if "daq_file_prefix" in scan_args and scan_args["daq_file_prefix"] is not None:
self.file_prefix.set(scan_args['daq_file_prefix']).wait()
self.file_prefix.set(scan_args["daq_file_prefix"]).wait()
if "daq_num_images" in scan_args and scan_args["daq_num_images"] is not None:
self.num_images.set(scan_args['daq_num_images']).wait()
self.num_images.set(scan_args["daq_num_images"]).wait()
# Start stdDAQ preview
if self.live_preview is not None:
self.live_preview.start()
@@ -632,10 +628,13 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
and self.trigger_mode == "auto"
and self.enable_mode == "soft"
):
t_start = time()
# BEC teststand operation mode: posedge of SoftEnable if Started
self.soft_enable.set(0).wait()
self.soft_enable.set(1).wait()
logger.info(f"Elapsed: {time()-t_start}")
if self.acquire_block.get():
wait_time = 0.2 + 0.001 * self.num_exposures.value * max(
self.acquire_time.value, self.acquire_period.value
@@ -647,8 +646,7 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
def on_complete(self) -> DeviceStatus | None:
"""Called to inquire if a device has completed a scans."""
# return self.acq_done()
return None
return self.acq_done()
def on_kickoff(self) -> DeviceStatus | None:
"""Called to kickoff a device for a fly scan. Has to be called explicitly."""
@@ -667,6 +665,6 @@ if __name__ == "__main__":
auto_soft_enable=True,
std_daq_ws="ws://129.129.95.111:8080",
std_daq_rest="http://129.129.95.111:5000",
std_daq_live='tcp://129.129.95.111:20000',
std_daq_live="tcp://129.129.95.111:20000",
)
gf.wait_for_connection()

View File

@@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
"""
Created on Wed Dec 6 11:33:54 2023
@author: mohacsi_i
"""
from ophyd import Component as Cpt
from ophyd import Device, DynamicDeviceComponent, EpicsSignal, EpicsSignalRO, Kind, Signal
class PcoEdgeBase(Device):
"""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
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).
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
UPDATE: Data sending operation modes
- Switch to ZMQ streaming by setting FILEFORMAT to ZEROMQ
- Set SAVESTART and SAVESTOP to select a ROI of image indices
- Start file transfer with FTRANSFER.
The ZMQ connection operates in PUSH-PULL mode, i.e. it needs incoming connection.
STOREMODE sets the acquisition mode:
if STOREMODE == Recorder
Fills up the buffer with images. Here SAVESTART and SAVESTOP selects a ROI
of image indices to be streamed out (i.e. maximum buffer_size number of images)
if STOREMODE == FIFO buffer
Continously streams out data using the buffer as a FIFO queue.
Here SAVESTART and SAVESTOP selects a ROI of image indices to be streamed continously
(i.e. a large SAVESTOP streams indefinitely). 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 = Cpt(EpicsSignalRO, "QUERY", kind=Kind.config, doc="Camera manufacturer info")
model = Cpt(EpicsSignalRO, "BOARD", kind=Kind.omitted, doc="Camera board info")
# ########################################################################
# Acquisition configuration (in AD nomenclature)
acquire = Cpt(EpicsSignal, "CAMERASTATUS", put_complete=True, kind=Kind.omitted)
acquire_time = Cpt(
EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config
)
acquire_delay = Cpt(
EpicsSignal, "DELAY", put_complete=True, auto_monitor=True, kind=Kind.config
)
trigger_mode = Cpt(
EpicsSignal, "TRIGGER", put_complete=True, auto_monitor=True, kind=Kind.config
)
# num_exposures = Cpt(
# EpicsSignal, "CNT_NUM", put_complete=True, auto_monitor=True, kind=Kind.config
# )
array_size = DynamicDeviceComponent(
{
"array_size_x": (EpicsSignal, "WIDTH", {"auto_monitor": True, "put_complete": True}),
"array_size_y": (EpicsSignal, "HEIGHT", {"auto_monitor": True, "put_complete": True}),
},
doc="Size of the array in the XY dimensions",
)
# DAQ parameters
file_path = Cpt(Signal, kind=Kind.config, value="/gpfs/test/test-beamline")
file_prefix = Cpt(Signal, kind=Kind.config, value="scan_")
num_images = Cpt(Signal, kind=Kind.config, value=1000)
num_images_counter = Cpt(Signal, kind=Kind.hinted, value=0)
# GF specific interface
acquire_block = Cpt(Signal, kind=Kind.config, value=0)
# ########################################################################
# Image size configuration (in AD nomenclature)
bin_x = Cpt(EpicsSignal, "BINX", put_complete=True, auto_monitor=True, kind=Kind.config)
bin_y = Cpt(EpicsSignal, "BINY", put_complete=True, auto_monitor=True, kind=Kind.config)
# ########################################################################
# Additional status info
busy = Cpt(EpicsSignalRO, "BUSY", auto_monitor=True, kind=Kind.config)
camState = Cpt(EpicsSignalRO, "SS_CAMERA", auto_monitor=True, kind=Kind.config)
camProgress = Cpt(EpicsSignalRO, "CAMPROGRESS", auto_monitor=True, kind=Kind.config)
# ########################################################################
# Configuration state maschine with separate transition states
set_param = Cpt(
EpicsSignal,
"BUSY_SET_PARAM",
write_pv="SET_PARAM",
put_complete=True,
auto_monitor=True,
kind=Kind.config,
)
camera_statuscode = Cpt(EpicsSignalRO, "STATUSCODE", auto_monitor=True, kind=Kind.config)
camera_init = Cpt(EpicsSignalRO, "INIT", auto_monitor=True, kind=Kind.config)
camera_init_busy = Cpt(EpicsSignalRO, "BUSY_INIT", auto_monitor=True, kind=Kind.config)
# camCamera = Cpt(EpicsSignalRO, "CAMERA", auto_monitor=True, kind=Kind.config)
# camCameraBusy = Component(EpicsSignalRO, "BUSY_CAMERA", auto_monitor=True, kind=Kind.config)
# ########################################################################
# Acquisition configuration
acquire_mode = Cpt(EpicsSignalRO, "ACQMODE", auto_monitor=True, kind=Kind.config)
acquire_trigger = Cpt(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)
# ########################################################################
# Buffer configuration
bufferRecMode = Cpt(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
bufferStoreMode = Cpt(EpicsSignal, "STOREMODE", auto_monitor=True, kind=Kind.config)
fileRecMode = Cpt(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
buffer_used = Cpt(EpicsSignalRO, "PIC_BUFFER", auto_monitor=True, kind=Kind.normal)
buffer_size = Cpt(EpicsSignalRO, "PIC_MAX", auto_monitor=True, kind=Kind.normal)
buffer_clear = Cpt(EpicsSignal, "CLEARMEM", put_complete=True, kind=Kind.omitted)
# ########################################################################
# File saving/streaming interface
cam_data_rate = Cpt(EpicsSignalRO, "CAMRATE", auto_monitor=True, kind=Kind.normal)
file_data_rate = Cpt(EpicsSignalRO, "FILERATE", auto_monitor=True, kind=Kind.normal)
file_savestart = Cpt(EpicsSignal, "SAVESTART", put_complete=True, kind=Kind.config)
file_savestop = Cpt(EpicsSignal, "SAVESTOP", put_complete=True, kind=Kind.config)
file_format = Cpt(EpicsSignal, "FILEFORMAT", put_complete=True, kind=Kind.config)
file_transfer = Cpt(EpicsSignal, "FTRANSFER", put_complete=True, kind=Kind.config)
file_savebusy = Cpt(EpicsSignalRO, "FILESAVEBUSY", auto_monitor=True, kind=Kind.normal)
# ########################################################################
# Throtled image preview
image = Cpt(EpicsSignalRO, "FPICTURE", kind=Kind.omitted, doc="Throttled image preview")
# ########################################################################
# General hardware info
camError = Cpt(EpicsSignalRO, "ERRCODE", auto_monitor=True, kind=Kind.config)
camWarning = Cpt(EpicsSignalRO, "WARNCODE", auto_monitor=True, kind=Kind.config)
@property
def state(self) -> str:
"""Single word camera state"""
if self.set_param.value:
return "BUSY"
if self.camera_statuscode.value == 2 and self.camera_init.value == 1:
return "IDLE"
if self.camera_statuscode.value == 6 and self.camera_init.value == 1:
return "RUNNING"
# if self.camRemoval.value==0 and self.camInit.value==0:
if self.camera_init.value == 0:
return "OFFLINE"
# if self.camRemoval.value:
# return "REMOVED"
return "UNKNOWN"
@state.setter
def state(self):
raise RuntimeError("State is a ReadOnly property")
# Automatically connect to test camera if directly invoked
if __name__ == "__main__":
# Drive data collection
cam = PcoEdgeBase("X02DA-CCDCAM2:", name="mcpcam")
cam.wait_for_connection()

View File

@@ -7,19 +7,16 @@ Created on Wed Dec 6 11:33:54 2023
import time
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind
from ophyd.status import SubscriptionStatus, DeviceStatus
from ophyd_devices import BECDeviceBase
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin as CustomPrepare,
)
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
try:
from bec_lib import bec_logger
from tomcat_bec.devices.gigafrost.pcoedge_base import PcoEdgeBase
from tomcat_bec.devices.gigafrost.std_daq_preview import StdDaqPreview
from tomcat_bec.devices.gigafrost.std_daq_client import StdDaqClient, StdDaqStatus
logger = bec_logger.logger
except ModuleNotFoundError:
import logging
logger = logging.getLogger("PcoEdgeCam")
from bec_lib.logger import bec_logger
logger = bec_logger.logger
class PcoEdgeCameraMixin(CustomPrepare):
@@ -27,119 +24,9 @@ class PcoEdgeCameraMixin(CustomPrepare):
This class will be called by the custom_prepare_cls attribute of the detector class.
"""
# pylint: disable=protected-access
def on_stage(self) -> None:
"""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!"
)
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 "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"]
# Perform bluesky-style configuration
if len(d) > 0:
logger.warning(f"[{self.parent.name}] Configuring with:\n{d}")
self.parent.configure(d=d)
# ARM the camera
self.parent.bluestage()
def on_unstage(self) -> None:
"""Disarm the PCO.Edge camera"""
self.parent.blueunstage()
def on_stop(self) -> None:
"""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.
"""
# 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 wait_bufferreset(*, old_value, value, timestamp, **_):
return (value < old_value) or (value == 0)
self.parent.buffer_clear.set(1).wait()
status = SubscriptionStatus(self.parent.buffer_used, wait_bufferreset, 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 wait_acquisition(*, value, timestamp, **_):
num_target = self.parent.file_savestop.get()
# logger.warning(f"{value} of {num_target}")
return bool(value >= num_target)
max_wait = max(5, 5 * t_expected)
status = SubscriptionStatus(
self.parent.buffer_used, wait_acquisition, timeout=max_wait, 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 wait_sending(*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, wait_sending, timeout=120, settle_time=0.2
)
status.wait()
class HelgeCameraBase(BECDeviceBase):
class PcoEdge5M(PSIDeviceBase, PcoEdgeBase):
"""Ophyd baseclass for Helge camera IOCs
This class provides wrappers for Helge's camera IOCs around SwissFEL and
@@ -175,105 +62,57 @@ class HelgeCameraBase(BECDeviceBase):
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")
# pylint: disable=too-many-instance-attributes
USER_ACCESS = [
"complete",
"backend",
# "acq_done",
"live_preview",
"arm",
"disarm",
]
# ########################################################################
# Acquisition commands
camStatusCmd = Component(EpicsSignal, "CAMERASTATUS", put_complete=True, kind=Kind.config)
# Placeholders for stdDAQ and livestream clients
backend = None
live_preview = None
# ########################################################################
# 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
)
# ########################################################################
# 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"
)
# ########################################################################
# General hardware info
camError = Component(EpicsSignalRO, "ERRCODE", auto_monitor=True, kind=Kind.config)
camWarning = Component(EpicsSignalRO, "WARNCODE", auto_monitor=True, kind=Kind.config)
# ########################################################################
# 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
cam_data_rate = Component(EpicsSignalRO, "CAMRATE", auto_monitor=True, kind=Kind.normal)
file_data_rate = Component(EpicsSignalRO, "FILERATE", auto_monitor=True, kind=Kind.normal)
file_savestart = Component(EpicsSignal, "SAVESTART", 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_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
)
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)
camInitBusy = Component(EpicsSignalRO, "BUSY_INIT", auto_monitor=True, kind=Kind.config)
# ########################################################################
# Throtled image preview
image = Component(EpicsSignalRO, "FPICTURE", kind=Kind.omitted, doc="Throttled image preview")
# ########################################################################
# 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
)
@property
def state(self) -> str:
"""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 "RUNNING"
# if self.camRemoval.value==0 and self.camInit.value==0:
if self.camInit.value == 0:
return "OFFLINE"
# if self.camRemoval.value:
# return "REMOVED"
return "UNKNOWN"
@state.setter
def state(self):
raise RuntimeError("State is a ReadOnly property")
def __init__(
self,
prefix="",
*,
name,
kind=None,
read_attrs=None,
configuration_attrs=None,
parent=None,
scan_info=None,
std_daq_rest: str | None = None,
std_daq_ws: str | None = None,
std_daq_live: str | None = None,
**kwargs,
):
# super() will call the mixin class
super().__init__(
prefix=prefix,
name=name,
kind=kind,
read_attrs=read_attrs,
configuration_attrs=configuration_attrs,
parent=parent,
scan_info=scan_info,
**kwargs,
)
# Configure the stdDAQ client
if std_daq_rest is None or std_daq_ws is None:
# raise ValueError("Both std_daq_rest and std_daq_ws must be provided")
logger.error("No stdDAQ address provided, launching without data backend!")
else:
self.backend = StdDaqClient(parent=self, ws_url=std_daq_ws, rest_url=std_daq_rest)
# Configure image preview
if std_daq_live is not None:
self.live_preview = StdDaqPreview(url=std_daq_live, cb=self._on_preview_update)
else:
logger.error("No stdDAQ stream address provided, launching without preview!")
def configure(self, d: dict = {}) -> tuple:
"""Configure the base Helge camera device
@@ -308,6 +147,10 @@ class HelgeCameraBase(BECDeviceBase):
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 "image_width" in d:
self.array_size.array_size_x.set(d["image_width"]).wait()
if "image_height" in d:
self.array_size.array_size_y.set(d["image_height"]).wait()
if "store_mode" in d:
self.bufferStoreMode.set(d["store_mode"]).wait()
if "data_format" in d:
@@ -320,16 +163,16 @@ class HelgeCameraBase(BECDeviceBase):
# 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 negedge(*, old_value, value, timestamp, **_):
return bool(old_value and not value)
# Subscribe and wait for update
status = SubscriptionStatus(self.camSetParam, negedge, timeout=5, settle_time=0.5)
status = SubscriptionStatus(self.set_param, negedge, timeout=5, settle_time=0.5)
self.set_param.set(1).wait()
status.wait()
def bluestage(self):
def arm(self):
"""Bluesky style stage: arm the detector"""
logger.warning("Staging PCO")
# Acquisition is only allowed when the IOC is not busy
@@ -354,7 +197,7 @@ class HelgeCameraBase(BECDeviceBase):
status = SubscriptionStatus(self.camStatusCode, is_running, timeout=5, settle_time=0.2)
status.wait()
def blueunstage(self):
def disarm(self):
"""Bluesky style unstage: stop the detector"""
self.camStatusCmd.set("Idle").wait()
@@ -362,107 +205,190 @@ class HelgeCameraBase(BECDeviceBase):
# FIXME: This might interrupt data transfer
self.file_savestop.set(0).wait()
def bluekickoff(self):
def destroy(self):
if self.backend is not None:
self.backend.shutdown()
super().destroy()
def _on_preview_update(self, img: np.ndarray, header: dict):
"""Send preview stream and update frame index counter"""
self.num_images_counter.put(header["frame"], force=True)
self._run_subs(sub_type=self.SUB_DEVICE_MONITOR_2D, obj=self, value=img)
def acq_done(self) -> DeviceStatus:
"""
Check if the acquisition is done. For the GigaFrost camera, this is
done by checking the status of the backend as the camera does not
provide any feedback about its internal state.
Returns:
DeviceStatus: The status of the acquisition
"""
status = DeviceStatus(self)
if self.backend is not None:
self.backend.add_status_callback(
status,
success=[StdDaqStatus.IDLE, StdDaqStatus.FILE_SAVED],
error=[StdDaqStatus.REJECTED, StdDaqStatus.ERROR],
)
return status
########################################
# Beamline Specific Implementations #
########################################
# pylint: disable=protected-access
def on_stage(self) -> None:
"""Configure and arm PCO.Edge camera for acquisition"""
# PCO can finish a run without explicit unstaging
if self.state not in ("IDLE"):
logger.warning(
f"Trying to stage the camera from state {self.state}, unstaging it first!"
)
self.unstage()
time.sleep(0.5)
# Fish out our configuration from scaninfo (via explicit or generic addressing)
scan_args = {
**self.scan_info.msg.request_inputs["inputs"],
**self.scan_info.msg.request_inputs["kwargs"],
**self.scan_info.msg.scan_parameters,
}
d = {}
if "exp_burst" in scan_args and scan_args["exp_burst"] is not None:
d["exposure_num_burst"] = scan_args["exp_burst"]
if "image_width" in scan_args and scan_args["image_width"] is not None:
d["image_width"] = scan_args["image_width"]
if "image_height" in scan_args and scan_args["image_height"] is not None:
d["image_height"] = scan_args["image_height"]
if "exp_time" in scan_args and scan_args["exp_time"] is not None:
d["exposure_time_ms"] = scan_args["exp_time"]
if "exp_period" in scan_args and scan_args["exp_period"] is not None:
d["exposure_period_ms"] = scan_args["exp_period"]
# if 'exp_burst' in scan_args and scan_args['exp_burst'] is not None:
# d['exposure_num_burst'] = scan_args['exp_burst']
# if 'acq_mode' in scan_args and scan_args['acq_mode'] is not None:
# d['acq_mode'] = scan_args['acq_mode']
# elif self.scaninfo.scan_type == "step":
# d['acq_mode'] = "default"
if "pco_store_mode" in scan_args and scan_args["pco_store_mode"] is not None:
d["store_mode"] = scan_args["pco_store_mode"]
if "pco_data_format" in scan_args and scan_args["pco_data_format"] is not None:
d["data_format"] = scan_args["pco_data_format"]
# Perform bluesky-style configuration
if d:
logger.warning(f"[{self.name}] Configuring with:\n{d}")
self.configure(d=d)
# stdDAQ backend parameters
num_points = (
1
* scan_args.get("steps", 1)
* scan_args.get("exp_burst", 1)
* scan_args.get("repeats", 1)
* scan_args.get("burst_at_each_point", 1)
)
self.num_images.set(num_points).wait()
if "daq_file_path" in scan_args and scan_args["daq_file_path"] is not None:
self.file_path.set(scan_args["daq_file_path"]).wait()
if "daq_file_prefix" in scan_args and scan_args["daq_file_prefix"] is not None:
self.file_prefix.set(scan_args["daq_file_prefix"]).wait()
if "daq_num_images" in scan_args and scan_args["daq_num_images"] is not None:
self.num_images.set(scan_args["daq_num_images"]).wait()
# Start stdDAQ preview
if self.live_preview is not None:
self.live_preview.start()
def on_unstage(self) -> None:
"""Disarm the PCO.Edge camera"""
self.disarm()
if self.backend is not None:
logger.info(f"StdDaq status before unstage: {self.backend.status}")
self.backend.stop()
def on_pre_scan(self) -> DeviceStatus | None:
"""Called right before the scan starts on all devices automatically."""
# First start the stdDAQ
if self.backend is not None:
self.backend.start(
file_path=self.file_path.get(),
file_prefix=self.file_prefix.get(),
num_images=self.num_images.get(),
)
# Then start the camera
self.arm()
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.
"""
# Ensure that previous data transfer finished
# def sentIt(*args, value, timestamp, **kwargs):
# return value==0
# status = SubscriptionStatus(self.file_savebusy, sentIt, timeout=120)
# status.wait()
# Not sure if it always sends the first batch of images or the newest
def wait_bufferreset(*, old_value, value, timestamp, **_):
return (value < old_value) or (value == 0)
self.buffer_clear.set(1).wait()
status = SubscriptionStatus(self.buffer_used, wait_bufferreset, timeout=5)
status.wait()
t_expected = (self.acquire_time.get() + self.acquire_delay.get()) * self.file_savestop.get()
# Wait until the buffer fills up with enough images
def wait_acquisition(*, value, timestamp, **_):
num_target = self.file_savestop.get()
# logger.warning(f"{value} of {num_target}")
return bool(value >= num_target)
max_wait = max(5, 5 * t_expected)
status = SubscriptionStatus(
self.buffer_used, wait_acquisition, timeout=max_wait, settle_time=0.2
)
status.wait()
# Then start file transfer (need to get the save busy flag update)
# self.file_transfer.set(1, settle_time=0.2).wait()
self.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 wait_sending(*, 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.file_savebusy, wait_sending, timeout=120, settle_time=0.2)
status.wait()
def on_complete(self) -> DeviceStatus | None:
"""Called to inquire if a device has completed a scans."""
return self.acq_done()
def on_kickoff(self) -> DeviceStatus | None:
"""Start data transfer
TODO: Need to revisit this once triggering is complete
"""
self.file_transfer.set(1).wait()
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
on a special Windows IOC host with lots of RAM and CPU power.
"""
custom_prepare_cls = PcoEdgeCameraMixin
USER_ACCESS = ["bluestage", "blueunstage", "bluekickoff"]
# ########################################################################
# Additional status info
busy = Component(EpicsSignalRO, "BUSY", auto_monitor=True, kind=Kind.config)
camState = Component(EpicsSignalRO, "SS_CAMERA", auto_monitor=True, kind=Kind.config)
camProgress = Component(EpicsSignalRO, "CAMPROGRESS", auto_monitor=True, kind=Kind.config)
camRate = Component(EpicsSignalRO, "CAMRATE", auto_monitor=True, kind=Kind.config)
# ########################################################################
# 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)
# ########################################################################
# 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
)
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
call SET_PARAM, but it might take long.
NOTE:
The camera IOC will automatically round up RoiX coordinates to the
next multiple of 160. This means that configure can only change image
width in steps of 320 pixels (or manually of 160). Roi
Parameters as 'd' dictionary
----------------------------
exposure_time_ms : float, optional
Exposure time [ms].
exposure_period_ms : float, optional
Exposure period [ms], ignored in soft trigger mode.
image_width : int, optional
ROI size in the x-direction, multiple of 320 [pixels]
image_height : int, optional
ROI size in the y-direction, multiple of 2 [pixels]
image_binx : int optional
Binning along image width [pixels]
image_biny: int, optional
Binning along image height [pixels]
acq_mode : str, not yet implemented
Select one of the pre-configured trigger behavior
"""
if d is not None:
# 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.bin_x.set(d["image_binx"]).wait()
if "image_biny" in d and d["image_biny"] is not None:
self.bin_y.set(d["image_biny"]).wait()
# Call super() to commit the changes
super().configure(d)
def on_stop(self) -> None:
"""Called when the device is stopped."""
return self.on_unstage()
# Automatically connect to test camera if directly invoked

View File

@@ -306,10 +306,10 @@ class StdDaqClient:
msg_timestamp = time.time()
except TimeoutError:
continue
except WebSocketException:
content = traceback.format_exc()
# TODO: this is expected to happen on every reconfiguration
logger.warning(f"Websocket connection closed unexpectedly: {content}")
except WebSocketException as ex:
# content = traceback.format_exc()
# TODO: ConnectionCloserError is expected to happen on every reconfiguration
logger.warning(f"Websocket connection closed unexpectedly: {ex}")
self.wait_for_connection()
continue
msg = json.loads(msg)