Work on Helge cameras

This commit is contained in:
gac-x05la
2025-01-30 17:19:43 +01:00
parent aa3636fd73
commit 0a1e11ed4f
2 changed files with 157 additions and 214 deletions
@@ -185,3 +185,16 @@ daq_stream1:
readOnly: false readOnly: false
readoutPriority: monitored readoutPriority: monitored
softwareTrigger: false softwareTrigger: false
pco_stream0:
description: Raw camera stream from PCO.edge
deviceClass: tomcat_bec.devices.StdDaqPreviewDetector
deviceConfig:
url: 'tcp://129.129.106.124:8080'
deviceTags:
- std-daq
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: async
softwareTrigger: false
+144 -214
View File
@@ -14,105 +14,71 @@ import time
from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase as PSIDeviceBase from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase as PSIDeviceBase
from ophyd_devices.interfaces.base_classes.psi_detector_base import CustomDetectorMixin as CustomDeviceMixin from ophyd_devices.interfaces.base_classes.psi_detector_base import CustomDetectorMixin as CustomDeviceMixin
try:
from bec_lib import bec_logger
logger = bec_logger.logger
except ModuleNotFoundError:
import logging
logger = logging.getLogger("PcoEdgeCam")
class HelgeCameraMixin(CustomDeviceMixin):
class PcoEdgeCameraMixin(CustomDeviceMixin):
"""Mixin class to setup the Helge camera bae class. """Mixin class to setup the Helge camera bae class.
This class will be called by the custom_prepare_cls attribute of the detector class. This class will be called by the custom_prepare_cls attribute of the detector class.
""" """
def on_stage(self) -> None:
def __init__(self, *_args, parent: Device = None, **_kwargs) -> None: """Configure and arm PCO.Edge camera for acquisition
super().__init__(*_args, parent=parent, **_kwargs)
self.monitor_thread = None
self.stop_monitor = False
self.update_frequency = 1
def set_exposure_time(self, exposure_time: float) -> None:
"""Set the detector framerate.
Args:
exposure_time (float): Desired exposure time in [sec]
"""
if exposure_time is not None:
self.parent.acqExpTime.set(exposure_time).wait()
def prepare_detector_backend(self) -> None:
pass
def prepare_detector(self) -> None:
"""Prepare detector for acquisition.
State machine:
BUSY and SET both low -> BUSY high, SET low -> BUSY low, SET high -> BUSY low, SET low
""" """
self.parent.camSetParam.set(1).wait() # Gigafrost can finish a run without explicit unstaging
def risingEdge(*args, old_value, value, timestamp, **kwargs): if self.parent.infoBusyFlag.value:
return bool(not old_value and value) logger.warning("Camera is already running, unstaging it first!")
def fallingEdge(*args, old_value, value, timestamp, **kwargs): self.parent.unstage()
return bool(old_value and not value) sleep(0.5)
# Subscribe and wait for update
status = SubscriptionStatus(self.parent.camSetParam, fallingEdge, settle_time=0.5)
status.wait()
# Fish out our configuration from scaninfo (via explicit or generic addressing)
scanparam = self.parent.scaninfo.scan_msg.info
alias = self.parent.parent.name if self.parent.parent is not None else self.parent.name
# logger.warning(f"[{alias}] Scan parameters:\n{scanparam}")
d = {}
if 'kwargs' in scanparam:
scanargs = scanparam['kwargs']
if 'image_width' in scanargs and scanargs['image_width']!=None:
d['image_width'] = scanargs['image_width']
if 'image_height' in scanargs and scanargs['image_height']!=None:
d['image_height'] = scanargs['image_height']
if 'exp_time' in scanargs and scanargs['exp_time']!=None:
d['exposure_time_ms'] = scanargs['exp_time']
if 'exp_period' in scanargs and scanargs['exp_period']!=None:
d['exposure_period_ms'] = scanargs['exp_period']
# if 'exp_burst' in scanargs and scanargs['exp_burst']!=None:
# d['exposure_num_burst'] = scanargs['exp_burst']
# if 'acq_mode' in scanargs and scanargs['acq_mode']!=None:
# d['acq_mode'] = scanargs['acq_mode']
# elif self.parent.scaninfo.scan_type == "step":
# d['acq_mode'] = "default"
def arm_acquisition(self) -> None: # Perform bluesky-style configuration
"""Arm camera for acquisition""" if len(d) > 0:
logger.warning(f"[{self.parent.name}] Configuring with:\n{d}")
self.parent.configure(d=d)
# Acquisition is only allowed when the IOC is not busy # ARM the camera
if self.parent.state in ("OFFLINE", "BUSY", "REMOVED", "RUNNING"): self.parent.bluestage()
raise RuntimeError(f"Camera in in state: {self.parent.state}")
# Start the acquisition (this sets parameers and starts acquisition) def on_unstage(self) -> None:
self.parent.camStatusCmd.set("Running").wait() """Disarm the PCO.Edge camera
# Subscribe and wait for update
def isRunning(*args, old_value, value, timestamp, **kwargs):
return bool(self.parent.state=="RUNNING")
status = SubscriptionStatus(self.parent.camStatusCode, isRunning, settle_time=0.2)
status.wait()
def stop_detector(self) -> None:
self.camStatusCmd.set("Idle").wait()
# Subscribe and wait for update
def isIdle(*args, old_value, value, timestamp, **kwargs):
return bool(value==2)
status = SubscriptionStatus(self.parent.camStatusCode, isIdle, settle_time=0.5)
status.wait()
def send_data(self) -> None:
"""Send data to monitor endpoint in redis."""
try:
img = self.parent.image.get()
# pylint: disable=protected-access
self.parent._run_subs(sub_type=self.parent.SUB_VALUE, value=img)
except Exception as e:
logger.debug(f"{e} for image with shape {self.parent.image.get().shape}")
def monitor_loop(self) -> None:
""" """
Monitor the detector status and send data. self.parent.blueunstage()
def on_stop(self) -> None:
"""Stop the PCO.Edge camera
""" """
while True: self.parent.blueunstage()
self.send_data()
time.sleep(1 / self.update_frequency)
if self.parent.state != "RUNNING":
break
if self.stop_monitor:
break
def run_monitor(self) -> None:
"""
Run the monitor loop in a separate thread.
"""
self.monitor_thread = threading.Thread(target=self.monitor_loop, daemon=True)
self.monitor_thread.start()
class HelgeCameraBase(PSIDeviceBase):
class HelgeCameraCore(PSIDeviceBase):
"""Ophyd baseclass for Helge camera IOCs """Ophyd baseclass for Helge camera IOCs
This class provides wrappers for Helge's camera IOCs around SwissFEL and This class provides wrappers for Helge's camera IOCs around SwissFEL and
@@ -122,33 +88,42 @@ class HelgeCameraCore(PSIDeviceBase):
Probably the most important part is the configuration state machine. As Probably the most important part is the configuration state machine. As
the SET_PARAMS takes care of buffer allocations it might take some time, the SET_PARAMS takes care of buffer allocations it might take some time,
as well as afull re-configuration is required every time we change the as well as a full re-configuration is required every time we change the
binning, roi, etc... This is automatically performed upon starting an binning, roi, etc... This is automatically performed upon starting an
exposure (if it heven't been done before). exposure (if it heven't been done before).
The status flag state machine during re-configuration is: The status flag state machine during re-configuration is:
BUSY low, SET low -> BUSY high, SET low -> BUSY low, SET high -> BUSY low, SET low BUSY low, SET low -> BUSY high, SET low -> BUSY low, SET high -> BUSY low, SET low
""" """
# Specify Mixin class # Specify Mixin class
custom_prepare_cls = HelgeCameraMixin custom_prepare_cls = HelgeCameraMixin
USER_ACCESS = ["kickoff"]
# ######################################################################## # ########################################################################
# General hardware info # General hardware info (in AD nomenclature)
camType = Component(EpicsSignalRO, "QUERY", kind=Kind.omitted) manufacturer = Component(EpicsSignalRO, "QUERY", kind=Kind.config, doc="Camera model info")
camBoard = Component(EpicsSignalRO, "BOARD", kind=Kind.config) model = Component(EpicsSignalRO, "BOARD", kind=Kind.omitted, doc="Camera board info")
camError = Component(EpicsSignalRO, "ERRCODE", auto_monitor=True, kind=Kind.config)
camWarning = Component(EpicsSignalRO, "WARNCODE", auto_monitor=True, kind=Kind.config)
# ######################################################################## # ########################################################################
# Acquisition commands # Acquisition commands
camStatusCmd = Component(EpicsSignal, "CAMERASTATUS", put_complete=True, kind=Kind.config) camStatusCmd = Component(EpicsSignal, "CAMERASTATUS", put_complete=True, kind=Kind.config)
# ######################################################################## # ########################################################################
# Acquisition configuration # Acquisition configuration (in AD nomenclature)
acqExpTime = Component(EpicsSignalRO, "EXPOSURE", auto_monitor=True, kind=Kind.config) acquire_time = Component(EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config)
acquire_delay = Component(EpicsSignal, "DELAY", put_complete=True, auto_monitor=True, kind=Kind.config)
trigger_mode = Component(EpicsSignal, "TRIGGER", put_complete=True, auto_monitor=True, kind=Kind.config)
# ########################################################################
# Image size configuration (in AD nomenclature)
bin_x = Component(EpicsSignal, "BINX", put_complete=True, auto_monitor=True, kind=Kind.config)
bin_y = Component(EpicsSignal, "BINY", put_complete=True, auto_monitor=True, kind=Kind.config)
array_size_x = Component(EpicsSignalRO, "WIDTH", auto_monitor=True, kind=Kind.config, doc="Final image width")
array_size_y = Component(EpicsSignalRO, "HEIGHT", auto_monitor=True, kind=Kind.config, doc="Final image height")
# ########################################################################
# General hardware info
camError = Component(EpicsSignalRO, "ERRCODE", auto_monitor=True, kind=Kind.config)
camWarning = Component(EpicsSignalRO, "WARNCODE", auto_monitor=True, kind=Kind.config)
# ######################################################################## # ########################################################################
# Configuration state maschine with separate transition states # Configuration state maschine with separate transition states
@@ -162,7 +137,7 @@ class HelgeCameraCore(PSIDeviceBase):
# ######################################################################## # ########################################################################
# Throtled image preview # Throtled image preview
image = Component(EpicsSignalRO, "FPICTURE", kind=Kind.omitted) image = Component(EpicsSignalRO, "FPICTURE", kind=Kind.omitted, doc="Throttled image preview")
# ######################################################################## # ########################################################################
# Misc PVs # Misc PVs
@@ -190,57 +165,78 @@ class HelgeCameraCore(PSIDeviceBase):
raise ReadOnlyError("State is a ReadOnly property") raise ReadOnlyError("State is a ReadOnly property")
def configure(self, d: dict = {}) -> tuple: def configure(self, d: dict = {}) -> tuple:
""" Configure the base Helge camera device"""
if self.state in ["OFFLINE", "REMOVED", "RUNNING"]: if self.state in ["OFFLINE", "REMOVED", "RUNNING"]:
raise RuntimeError(f"Can't change configuration from state {self.state}") raise RuntimeError(f"Can't change configuration from state {self.state}")
def stage(self) -> list[object]: # Stop acquisition
""" Start acquisition""" self.unstage()
self.custom_prepare.arm_acquisition() if not self._initialized:
return super().stage() pass
# If Bluesky style configure
if d is not None:
# Commonly changed settings
if 'exposure_time_ms' in d:
self.acquire_time.set(d['exposure_time_ms']).wait()
if 'exposure_period_ms' in d:
# acquire_time = d['exposure_time_ms'] if 'exposure_time_ms' in d else self.acquire_time.get()
self.acquire_delay.set(d['exposure_period_ms']).wait()
# State machine
# Initial: BUSY and SET both low
# 0. Write 1 to SET_PARAM
# 1. BUSY goes high, SET stays low
# 2. BUSY goes low, SET goes high
# 3. BUSY stays low, SET goes low
# So we need a 'negedge' on SET_PARAM
self.camSetParam.set(1).wait()
def fallingEdge(*args, old_value, value, timestamp, **kwargs):
return bool(old_value and not value)
# Subscribe and wait for update
status = SubscriptionStatus(self.camSetParam, fallingEdge, timeout=5, settle_time=0.5)
status.wait()
def kickoff(self) -> DeviceStatus: def bluestage(self):
""" Start acquisition""" """Bluesky style stage: arm the detector
return self.stage() """
# Acquisition is only allowed when the IOC is not busy
if self.state in ("OFFLINE", "BUSY", "REMOVED", "RUNNING"):
raise RuntimeError(f"Camera in in state: {self.state}")
def stop(self): # Start the acquisition (this sets parameers and starts acquisition)
""" Stop the running acquisition """ self.camStatusCmd.set("Running").wait()
# Subscribe and wait for update
def isRunning(*args, old_value, value, timestamp, **kwargs):
return bool(self.state=="RUNNING")
status = SubscriptionStatus(self.camStatusCode, isRunning, timeout=5, settle_time=0.2)
status.wait()
def blueunstage(self):
"""Bluesky style unstage: stop the detector
"""
self.camStatusCmd.set("Idle").wait() self.camStatusCmd.set("Idle").wait()
self.custom_prepare.stop_monitor = True self.custom_prepare.stop_monitor = True
return super().unstage()
def unstage(self): # Subscribe and wait for update
""" Stop the running acquisition and unstage the device""" def isIdle(*args, old_value, value, timestamp, **kwargs):
self.camStatusCmd.set("Idle").wait() return bool(value==2)
self.custom_prepare.stop_monitor = True status = SubscriptionStatus(self.parent.camStatusCode, isIdle, timeout=5, settle_time=0.2)
return super().unstage() status.wait()
class PcoEdgeBase(HelgeCameraBase):
"""Ophyd baseclass for PCO.Edge cameras
class HelgeCameraBase(HelgeCameraCore):
"""Ophyd baseclass for Helge camera IOCs
This class provides wrappers for Helge's camera IOCs around SwissFEL and This class provides wrappers for Helge's camera IOCs around SwissFEL and
for high performance SLS 2.0 cameras. Theese are mostly PCO cameras running for high performance SLS 2.0 cameras. Theese are mostly PCO cameras running
on a special Windows IOC host with lots of RAM and CPU power. on a special Windows IOC host with lots of RAM and CPU power.
The IOC's operation is a bit arcane, and is documented on the "read the code"
level. However the most important part is the state machine of 7+1 PV signals:
INIT
BUSY_INIT
SET_PARAM
BUSY_SET_PARAM
CAMERA
BUSY_CAMERA
CAMERASTATUSCODE
CAMERASTATUS
""" """
custom_prepare_cls = PcoEdgeCameraMixin
USER_ACCESS = ["bluestage", "blueunstage"]
USER_ACCESS = ["describe", "shape", "bin", "roi"]
# ######################################################################## # ########################################################################
# Additional status info # Additional status info
busy = Component(EpicsSignalRO, "BUSY", auto_monitor=True, kind=Kind.config) busy = Component(EpicsSignalRO, "BUSY", auto_monitor=True, kind=Kind.config)
@@ -259,15 +255,10 @@ class HelgeCameraBase(HelgeCameraCore):
# ######################################################################## # ########################################################################
# Image size settings # Image size settings
# Priority is: binning -> roi -> final size # Priority is: binning -> roi -> final size
pxBinX = Component(EpicsSignal, "BINX", put_complete=True, auto_monitor=True, kind=Kind.config)
pxBinY = Component(EpicsSignal, "BINY", put_complete=True, auto_monitor=True, kind=Kind.config)
pxRoiX_lo = Component(EpicsSignal, "REGIONX_START", put_complete=True, auto_monitor=True, kind=Kind.config) pxRoiX_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) 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_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) pxRoiY_hi = Component(EpicsSignal, "REGIONY_END", put_complete=True, auto_monitor=True, kind=Kind.config)
pxNumX = Component(EpicsSignalRO, "WIDTH", auto_monitor=True, kind=Kind.config)
pxNumY = Component(EpicsSignalRO, "HEIGHT", auto_monitor=True, kind=Kind.config)
# ######################################################################## # ########################################################################
# Buffer configuration # Buffer configuration
@@ -275,16 +266,6 @@ class HelgeCameraBase(HelgeCameraCore):
bufferStoreMode = Component(EpicsSignalRO, "STOREMODE", auto_monitor=True, kind=Kind.config) bufferStoreMode = Component(EpicsSignalRO, "STOREMODE", auto_monitor=True, kind=Kind.config)
fileRecMode = Component(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config) fileRecMode = Component(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
# ########################################################################
# File interface
camFileFormat = Component(EpicsSignal, "FILEFORMAT", put_complete=True, kind=Kind.config)
camFilePath = Component(EpicsSignal, "FILEPATH", put_complete=True, kind=Kind.config)
camFileName = Component(EpicsSignal, "FILENAME", put_complete=True, kind=Kind.config)
camFileNr = Component(EpicsSignal, "FILENR", put_complete=True, kind=Kind.config)
camFilePath = Component(EpicsSignal, "FILEPATH", put_complete=True, kind=Kind.config)
camFileTransferStart = Component(EpicsSignal, "FTRANSFER", put_complete=True, kind=Kind.config)
camFileTransferStop = Component(EpicsSignal, "SAVESTOP", put_complete=True, kind=Kind.config)
def configure(self, d: dict = {}) -> tuple: def configure(self, d: dict = {}) -> tuple:
@@ -297,77 +278,26 @@ class HelgeCameraBase(HelgeCameraCore):
call SET_PARAM, but it might take long. call SET_PARAM, but it might take long.
""" """
old = self.read_configuration() old = self.read_configuration()
super().configure(d)
if "exptime" in d: if d is not None:
exposure_time = d['exptime'] # Need to be smart how we set the ROI....
if exposure_time is not None: # Image sensor is 2560x2160 (X and Y)
self.acqExpTime.set(exposure_time).wait()
if "roi" in d:
roi = d["roi"]
if not isinstance(roi, (list, tuple)):
raise ValueError(f"Unknown ROI data type {type(roi)}")
if not len(roi[0])==2 and len(roi[1])==2:
raise ValueError(f"Unknown ROI shape: {roi}")
# Values are rounded to multiples of 16 # Values are rounded to multiples of 16
self.pxRoiX_lo.set(roi[0][0]).wait() if 'image_width' in d and d['image_width'] is not None:
self.pxRoiX_hi.set(roi[0][1]).wait() width = d['image_width']
self.pxRoiY_lo.set(roi[1][0]).wait() self.pxRoiX_lo.set(2560/2-width/2).wait()
self.pxRoiY_hi.set(roi[1][1]).wait() self.pxRoiX_hi.set(2560/2+width/2).wait()
if 'image_height' in d and d['image_height'] is not None:
if "bin" in d: height = d['image_height']
binning = d["bin"] self.pxRoiY_lo.set(2160/2-height/2).wait()
if not isinstance(binning, (list, tuple)): self.pxRoiY_hi.set(2160/2+height/2).wait()
raise ValueError(f"Unknown BINNING data type {type(binning)}") if 'image_binx' in d and d['image_binx'] is not None:
if not len(binning)==2: self.pxBinX.set(d['image_binx']).wait()
raise ValueError(f"Unknown ROI shape: {binning}") if 'image_biny' in d and d['image_biny'] is not None:
self.pxBinX.set(binning[0]).wait() self.pxBinY.set(d['image_biny']).wait()
self.pxBinY.set(binning[1]).wait()
# State machine
# Initial: BUSY and SET both low
# 1. BUSY set to high
# 2. BUSY goes low, SET goes high
# 3. SET goes low
self.camSetParam.set(1).wait()
def risingEdge(*args, old_value, value, timestamp, **kwargs):
return bool(not old_value and value)
def fallingEdge(*args, old_value, value, timestamp, **kwargs):
return bool(old_value and not value)
# Subscribe and wait for update
status = SubscriptionStatus(self.camSetParam, fallingEdge, settle_time=0.5)
status.wait()
new = self.read_configuration()
return (old, new)
@property
def shape(self):
return (int(self.pxNumX.value), int(self.pxNumY.value))
@shape.setter
def shape(self):
raise ReadOnlyError("Shape is a ReadOnly property")
@property
def bin(self):
return (int(self.pxBinX.value), int(self.pxBinY.value))
@bin.setter
def bin(self):
raise ReadOnlyError("Bin is a ReadOnly property")
@property
def roi(self):
return ((int(self.pxRoiX_lo.value), int(self.pxRoiX_hi.value)), (int(self.pxRoiY_lo.value), int(self.pxRoiY_hi.value)))
@roi.setter
def roi(self):
raise ReadOnlyError("Roi is a ReadOnly property")
# Call super() to commit the changes
super().configure(d)