Work on Helge cameras
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user