GF seems done, working on PCO
This commit is contained in:
@@ -6,22 +6,17 @@ Created on Thu Jun 27 17:28:43 2024
|
|||||||
|
|
||||||
@author: mohacsi_i
|
@author: mohacsi_i
|
||||||
"""
|
"""
|
||||||
from time import sleep
|
from time import sleep, time
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from ophyd import DeviceStatus
|
from ophyd import DeviceStatus
|
||||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
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
|
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_preview import StdDaqPreview
|
||||||
|
from tomcat_bec.devices.gigafrost.std_daq_client import StdDaqClient, StdDaqStatus
|
||||||
|
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@@ -62,6 +57,7 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
|||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
# pylint: disable=too-many-instance-attributes
|
||||||
USER_ACCESS = [
|
USER_ACCESS = [
|
||||||
|
"complete",
|
||||||
"exposure_mode",
|
"exposure_mode",
|
||||||
"fix_nframes_mode",
|
"fix_nframes_mode",
|
||||||
"trigger_mode",
|
"trigger_mode",
|
||||||
@@ -182,13 +178,13 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
|||||||
if d and self.backend is not None:
|
if d and self.backend is not None:
|
||||||
daq_update = {}
|
daq_update = {}
|
||||||
if "image_height" in d:
|
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:
|
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:
|
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:
|
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:
|
if daq_update:
|
||||||
self.backend.set_config(daq_update, force=False)
|
self.backend.set_config(daq_update, force=False)
|
||||||
@@ -493,28 +489,28 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
|||||||
self.backend.shutdown()
|
self.backend.shutdown()
|
||||||
super().destroy()
|
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"""
|
"""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)
|
self._run_subs(sub_type=self.SUB_DEVICE_MONITOR_2D, obj=self, value=img)
|
||||||
|
|
||||||
# def acq_done(self) -> DeviceStatus:
|
def acq_done(self) -> DeviceStatus:
|
||||||
# """
|
"""
|
||||||
# Check if the acquisition is done. For the GigaFrost camera, this is
|
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
|
done by checking the status of the backend as the camera does not
|
||||||
# provide any feedback about its internal state.
|
provide any feedback about its internal state.
|
||||||
|
|
||||||
# Returns:
|
Returns:
|
||||||
# DeviceStatus: The status of the acquisition
|
DeviceStatus: The status of the acquisition
|
||||||
# """
|
"""
|
||||||
# status = DeviceStatus(self)
|
status = DeviceStatus(self)
|
||||||
# if self.backend is not None:
|
if self.backend is not None:
|
||||||
# self.backend.add_status_callback(
|
self.backend.add_status_callback(
|
||||||
# status,
|
status,
|
||||||
# success=[StdDaqStatus.IDLE, StdDaqStatus.FILE_SAVED],
|
success=[StdDaqStatus.IDLE, StdDaqStatus.FILE_SAVED],
|
||||||
# error=[StdDaqStatus.REJECTED, StdDaqStatus.ERROR],
|
error=[StdDaqStatus.REJECTED, StdDaqStatus.ERROR],
|
||||||
# )
|
)
|
||||||
# return status
|
return status
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
# Beamline Specific Implementations #
|
# Beamline Specific Implementations #
|
||||||
@@ -590,11 +586,11 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
|||||||
)
|
)
|
||||||
self.num_images.set(num_points).wait()
|
self.num_images.set(num_points).wait()
|
||||||
if "daq_file_path" in scan_args and scan_args["daq_file_path"] is not None:
|
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:
|
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:
|
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
|
# Start stdDAQ preview
|
||||||
if self.live_preview is not None:
|
if self.live_preview is not None:
|
||||||
self.live_preview.start()
|
self.live_preview.start()
|
||||||
@@ -632,10 +628,13 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
|||||||
and self.trigger_mode == "auto"
|
and self.trigger_mode == "auto"
|
||||||
and self.enable_mode == "soft"
|
and self.enable_mode == "soft"
|
||||||
):
|
):
|
||||||
|
t_start = time()
|
||||||
# BEC teststand operation mode: posedge of SoftEnable if Started
|
# BEC teststand operation mode: posedge of SoftEnable if Started
|
||||||
self.soft_enable.set(0).wait()
|
self.soft_enable.set(0).wait()
|
||||||
self.soft_enable.set(1).wait()
|
self.soft_enable.set(1).wait()
|
||||||
|
|
||||||
|
logger.info(f"Elapsed: {time()-t_start}")
|
||||||
|
|
||||||
if self.acquire_block.get():
|
if self.acquire_block.get():
|
||||||
wait_time = 0.2 + 0.001 * self.num_exposures.value * max(
|
wait_time = 0.2 + 0.001 * self.num_exposures.value * max(
|
||||||
self.acquire_time.value, self.acquire_period.value
|
self.acquire_time.value, self.acquire_period.value
|
||||||
@@ -647,8 +646,7 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
|||||||
|
|
||||||
def on_complete(self) -> DeviceStatus | None:
|
def on_complete(self) -> DeviceStatus | None:
|
||||||
"""Called to inquire if a device has completed a scans."""
|
"""Called to inquire if a device has completed a scans."""
|
||||||
# return self.acq_done()
|
return self.acq_done()
|
||||||
return None
|
|
||||||
|
|
||||||
def on_kickoff(self) -> DeviceStatus | None:
|
def on_kickoff(self) -> DeviceStatus | None:
|
||||||
"""Called to kickoff a device for a fly scan. Has to be called explicitly."""
|
"""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,
|
auto_soft_enable=True,
|
||||||
std_daq_ws="ws://129.129.95.111:8080",
|
std_daq_ws="ws://129.129.95.111:8080",
|
||||||
std_daq_rest="http://129.129.95.111:5000",
|
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()
|
gf.wait_for_connection()
|
||||||
|
|||||||
176
tomcat_bec/devices/gigafrost/pcoedge_base.py
Normal file
176
tomcat_bec/devices/gigafrost/pcoedge_base.py
Normal 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()
|
||||||
@@ -7,19 +7,16 @@ Created on Wed Dec 6 11:33:54 2023
|
|||||||
import time
|
import time
|
||||||
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind
|
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind
|
||||||
from ophyd.status import SubscriptionStatus, DeviceStatus
|
from ophyd.status import SubscriptionStatus, DeviceStatus
|
||||||
from ophyd_devices import BECDeviceBase
|
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
|
||||||
CustomDetectorMixin as CustomPrepare,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
from tomcat_bec.devices.gigafrost.pcoedge_base import PcoEdgeBase
|
||||||
from bec_lib import bec_logger
|
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):
|
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.
|
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
|
"""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
|
||||||
@@ -175,105 +62,57 @@ class HelgeCameraBase(BECDeviceBase):
|
|||||||
destructive. to prevent this, we don't have EPICS preview
|
destructive. to prevent this, we don't have EPICS preview
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# ########################################################################
|
# pylint: disable=too-many-instance-attributes
|
||||||
# General hardware info (in AD nomenclature)
|
USER_ACCESS = [
|
||||||
manufacturer = Component(EpicsSignalRO, "QUERY", kind=Kind.config, doc="Camera model info")
|
"complete",
|
||||||
model = Component(EpicsSignalRO, "BOARD", kind=Kind.omitted, doc="Camera board info")
|
"backend",
|
||||||
|
# "acq_done",
|
||||||
|
"live_preview",
|
||||||
|
"arm",
|
||||||
|
"disarm",
|
||||||
|
]
|
||||||
|
|
||||||
# ########################################################################
|
# Placeholders for stdDAQ and livestream clients
|
||||||
# Acquisition commands
|
backend = None
|
||||||
camStatusCmd = Component(EpicsSignal, "CAMERASTATUS", put_complete=True, kind=Kind.config)
|
live_preview = None
|
||||||
|
|
||||||
# ########################################################################
|
def __init__(
|
||||||
# Acquisition configuration (in AD nomenclature)
|
self,
|
||||||
acquire_time = Component(
|
prefix="",
|
||||||
EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config
|
*,
|
||||||
)
|
name,
|
||||||
acquire_delay = Component(
|
kind=None,
|
||||||
EpicsSignal, "DELAY", put_complete=True, auto_monitor=True, kind=Kind.config
|
read_attrs=None,
|
||||||
)
|
configuration_attrs=None,
|
||||||
trigger_mode = Component(
|
parent=None,
|
||||||
EpicsSignal, "TRIGGER", put_complete=True, auto_monitor=True, kind=Kind.config
|
scan_info=None,
|
||||||
)
|
std_daq_rest: str | None = None,
|
||||||
|
std_daq_ws: str | None = None,
|
||||||
# ########################################################################
|
std_daq_live: str | None = None,
|
||||||
# Image size configuration (in AD nomenclature)
|
**kwargs,
|
||||||
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)
|
# super() will call the mixin class
|
||||||
array_size_x = Component(
|
super().__init__(
|
||||||
EpicsSignalRO, "WIDTH", auto_monitor=True, kind=Kind.config, doc="Final image width"
|
prefix=prefix,
|
||||||
)
|
name=name,
|
||||||
array_size_y = Component(
|
kind=kind,
|
||||||
EpicsSignalRO, "HEIGHT", auto_monitor=True, kind=Kind.config, doc="Final image height"
|
read_attrs=read_attrs,
|
||||||
)
|
configuration_attrs=configuration_attrs,
|
||||||
|
parent=parent,
|
||||||
# ########################################################################
|
scan_info=scan_info,
|
||||||
# General hardware info
|
**kwargs,
|
||||||
camError = Component(EpicsSignalRO, "ERRCODE", auto_monitor=True, kind=Kind.config)
|
)
|
||||||
camWarning = Component(EpicsSignalRO, "WARNCODE", auto_monitor=True, kind=Kind.config)
|
# 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")
|
||||||
# Buffer configuration
|
logger.error("No stdDAQ address provided, launching without data backend!")
|
||||||
bufferRecMode = Component(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
|
else:
|
||||||
bufferStoreMode = Component(EpicsSignal, "STOREMODE", auto_monitor=True, kind=Kind.config)
|
self.backend = StdDaqClient(parent=self, ws_url=std_daq_ws, rest_url=std_daq_rest)
|
||||||
fileRecMode = Component(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
|
# Configure image preview
|
||||||
|
if std_daq_live is not None:
|
||||||
buffer_used = Component(EpicsSignalRO, "PIC_BUFFER", auto_monitor=True, kind=Kind.normal)
|
self.live_preview = StdDaqPreview(url=std_daq_live, cb=self._on_preview_update)
|
||||||
buffer_size = Component(EpicsSignalRO, "PIC_MAX", auto_monitor=True, kind=Kind.normal)
|
else:
|
||||||
buffer_clear = Component(EpicsSignal, "CLEARMEM", put_complete=True, kind=Kind.omitted)
|
logger.error("No stdDAQ stream address provided, launching without preview!")
|
||||||
|
|
||||||
# ########################################################################
|
|
||||||
# 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 configure(self, d: dict = {}) -> tuple:
|
def configure(self, d: dict = {}) -> tuple:
|
||||||
"""Configure the base Helge camera device
|
"""Configure the base Helge camera device
|
||||||
@@ -308,6 +147,10 @@ class HelgeCameraBase(BECDeviceBase):
|
|||||||
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 "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:
|
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:
|
||||||
@@ -320,16 +163,16 @@ class HelgeCameraBase(BECDeviceBase):
|
|||||||
# 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()
|
|
||||||
|
|
||||||
def negedge(*, old_value, value, timestamp, **_):
|
def negedge(*, old_value, value, timestamp, **_):
|
||||||
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, 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()
|
status.wait()
|
||||||
|
|
||||||
def bluestage(self):
|
def arm(self):
|
||||||
"""Bluesky style stage: arm the detector"""
|
"""Bluesky style stage: arm the detector"""
|
||||||
logger.warning("Staging PCO")
|
logger.warning("Staging PCO")
|
||||||
# Acquisition is only allowed when the IOC is not busy
|
# 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 = SubscriptionStatus(self.camStatusCode, is_running, timeout=5, settle_time=0.2)
|
||||||
status.wait()
|
status.wait()
|
||||||
|
|
||||||
def blueunstage(self):
|
def disarm(self):
|
||||||
"""Bluesky style unstage: stop the detector"""
|
"""Bluesky style unstage: stop the detector"""
|
||||||
self.camStatusCmd.set("Idle").wait()
|
self.camStatusCmd.set("Idle").wait()
|
||||||
|
|
||||||
@@ -362,107 +205,190 @@ class HelgeCameraBase(BECDeviceBase):
|
|||||||
# FIXME: This might interrupt data transfer
|
# FIXME: This might interrupt data transfer
|
||||||
self.file_savestop.set(0).wait()
|
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
|
"""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 on_stop(self) -> None:
|
||||||
class PcoEdge5M(HelgeCameraBase):
|
"""Called when the device is stopped."""
|
||||||
"""Ophyd baseclass for PCO.Edge cameras
|
return self.on_unstage()
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# Automatically connect to test camera if directly invoked
|
# Automatically connect to test camera if directly invoked
|
||||||
|
|||||||
@@ -306,10 +306,10 @@ class StdDaqClient:
|
|||||||
msg_timestamp = time.time()
|
msg_timestamp = time.time()
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
continue
|
continue
|
||||||
except WebSocketException:
|
except WebSocketException as ex:
|
||||||
content = traceback.format_exc()
|
# content = traceback.format_exc()
|
||||||
# TODO: this is expected to happen on every reconfiguration
|
# TODO: ConnectionCloserError is expected to happen on every reconfiguration
|
||||||
logger.warning(f"Websocket connection closed unexpectedly: {content}")
|
logger.warning(f"Websocket connection closed unexpectedly: {ex}")
|
||||||
self.wait_for_connection()
|
self.wait_for_connection()
|
||||||
continue
|
continue
|
||||||
msg = json.loads(msg)
|
msg = json.loads(msg)
|
||||||
|
|||||||
Reference in New Issue
Block a user