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

View File

@@ -185,3 +185,16 @@ daq_stream1:
readOnly: false
readoutPriority: monitored
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

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 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.
This class will be called by the custom_prepare_cls attribute of the detector class.
"""
def __init__(self, *_args, parent: Device = None, **_kwargs) -> None:
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
def on_stage(self) -> None:
"""Configure and arm PCO.Edge camera for acquisition
"""
self.parent.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.parent.camSetParam, fallingEdge, settle_time=0.5)
status.wait()
# Gigafrost can finish a run without explicit unstaging
if self.parent.infoBusyFlag.value:
logger.warning("Camera is already running, unstaging it first!")
self.parent.unstage()
sleep(0.5)
# 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:
"""Arm camera for acquisition"""
# Perform bluesky-style configuration
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
if self.parent.state in ("OFFLINE", "BUSY", "REMOVED", "RUNNING"):
raise RuntimeError(f"Camera in in state: {self.parent.state}")
# ARM the camera
self.parent.bluestage()
# Start the acquisition (this sets parameers and starts acquisition)
self.parent.camStatusCmd.set("Running").wait()
# 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:
def on_unstage(self) -> None:
"""Disarm the PCO.Edge camera
"""
Monitor the detector status and send data.
self.parent.blueunstage()
def on_stop(self) -> None:
"""Stop the PCO.Edge camera
"""
while True:
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()
self.parent.blueunstage()
class HelgeCameraCore(PSIDeviceBase):
class HelgeCameraBase(PSIDeviceBase):
"""Ophyd baseclass for Helge camera IOCs
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
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
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:
BUSY low, SET low -> BUSY high, SET low -> BUSY low, SET high -> BUSY low, SET low
"""
# Specify Mixin class
custom_prepare_cls = HelgeCameraMixin
USER_ACCESS = ["kickoff"]
# ########################################################################
# General hardware info
camType = Component(EpicsSignalRO, "QUERY", kind=Kind.omitted)
camBoard = Component(EpicsSignalRO, "BOARD", kind=Kind.config)
camError = Component(EpicsSignalRO, "ERRCODE", auto_monitor=True, kind=Kind.config)
camWarning = Component(EpicsSignalRO, "WARNCODE", auto_monitor=True, kind=Kind.config)
# 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")
# ########################################################################
# Acquisition commands
camStatusCmd = Component(EpicsSignal, "CAMERASTATUS", put_complete=True, kind=Kind.config)
# ########################################################################
# Acquisition configuration
acqExpTime = Component(EpicsSignalRO, "EXPOSURE", auto_monitor=True, kind=Kind.config)
# 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)
# ########################################################################
# Configuration state maschine with separate transition states
@@ -162,7 +137,7 @@ class HelgeCameraCore(PSIDeviceBase):
# ########################################################################
# Throtled image preview
image = Component(EpicsSignalRO, "FPICTURE", kind=Kind.omitted)
image = Component(EpicsSignalRO, "FPICTURE", kind=Kind.omitted, doc="Throttled image preview")
# ########################################################################
# Misc PVs
@@ -190,57 +165,78 @@ class HelgeCameraCore(PSIDeviceBase):
raise ReadOnlyError("State is a ReadOnly property")
def configure(self, d: dict = {}) -> tuple:
""" Configure the base Helge camera device"""
if self.state in ["OFFLINE", "REMOVED", "RUNNING"]:
raise RuntimeError(f"Can't change configuration from state {self.state}")
def stage(self) -> list[object]:
""" Start acquisition"""
self.custom_prepare.arm_acquisition()
return super().stage()
# Stop acquisition
self.unstage()
if not self._initialized:
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:
""" Start acquisition"""
return self.stage()
def bluestage(self):
"""Bluesky style stage: arm the detector
"""
# 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):
""" Stop the running acquisition """
# Start the acquisition (this sets parameers and starts 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.custom_prepare.stop_monitor = True
return super().unstage()
def unstage(self):
""" Stop the running acquisition and unstage the device"""
self.camStatusCmd.set("Idle").wait()
self.custom_prepare.stop_monitor = True
return super().unstage()
# Subscribe and wait for update
def isIdle(*args, old_value, value, timestamp, **kwargs):
return bool(value==2)
status = SubscriptionStatus(self.parent.camStatusCode, isIdle, timeout=5, settle_time=0.2)
status.wait()
class HelgeCameraBase(HelgeCameraCore):
"""Ophyd baseclass for Helge camera IOCs
class PcoEdgeBase(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.
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
busy = Component(EpicsSignalRO, "BUSY", auto_monitor=True, kind=Kind.config)
@@ -259,15 +255,10 @@ class HelgeCameraBase(HelgeCameraCore):
# ########################################################################
# Image size settings
# 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_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)
pxNumX = Component(EpicsSignalRO, "WIDTH", auto_monitor=True, kind=Kind.config)
pxNumY = Component(EpicsSignalRO, "HEIGHT", auto_monitor=True, kind=Kind.config)
# ########################################################################
# Buffer configuration
@@ -275,16 +266,6 @@ class HelgeCameraBase(HelgeCameraCore):
bufferStoreMode = Component(EpicsSignalRO, "STOREMODE", 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:
@@ -297,77 +278,26 @@ class HelgeCameraBase(HelgeCameraCore):
call SET_PARAM, but it might take long.
"""
old = self.read_configuration()
super().configure(d)
if "exptime" in d:
exposure_time = d['exptime']
if exposure_time is not None:
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}")
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
self.pxRoiX_lo.set(roi[0][0]).wait()
self.pxRoiX_hi.set(roi[0][1]).wait()
self.pxRoiY_lo.set(roi[1][0]).wait()
self.pxRoiY_hi.set(roi[1][1]).wait()
if "bin" in d:
binning = d["bin"]
if not isinstance(binning, (list, tuple)):
raise ValueError(f"Unknown BINNING data type {type(binning)}")
if not len(binning)==2:
raise ValueError(f"Unknown ROI shape: {binning}")
self.pxBinX.set(binning[0]).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")
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.pxBinX.set(d['image_binx']).wait()
if 'image_biny' in d and d['image_biny'] is not None:
self.pxBinY.set(d['image_biny']).wait()
# Call super() to commit the changes
super().configure(d)