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
|
||||
"""
|
||||
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()
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user