From b9c7ab93ac60618127cda77267247f79a429b0a5 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Thu, 23 Jan 2025 12:55:07 +0100 Subject: [PATCH 01/14] Old Helge camera device can be instantiated again --- tomcat_bec/devices/gigafrost/helgecamera.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/helgecamera.py b/tomcat_bec/devices/gigafrost/helgecamera.py index f6067e0..28fd8e3 100644 --- a/tomcat_bec/devices/gigafrost/helgecamera.py +++ b/tomcat_bec/devices/gigafrost/helgecamera.py @@ -11,10 +11,11 @@ from time import sleep import warnings import numpy as np import time -from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase +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 -class HelgeCameraMixin(CustomDetectorMixin): +class HelgeCameraMixin(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. @@ -111,10 +112,10 @@ class HelgeCameraMixin(CustomDetectorMixin): -class HelgeCameraCore(PSIDetectorBase): +class HelgeCameraCore(PSIDeviceBase): """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. 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. @@ -378,7 +379,7 @@ class HelgeCameraBase(HelgeCameraCore): if __name__ == "__main__": # Drive data collection - cam = HelgeCameraBase("SINBC02-DSRM310:", name="mcpcam") + cam = HelgeCameraBase("X02DA-CCDCAM2:", name="mcpcam") cam.wait_for_connection() -- 2.49.1 From aa3636fd73a241131a818f814dac29aa673bf376 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Thu, 23 Jan 2025 13:17:48 +0100 Subject: [PATCH 02/14] Tested AD style array_size on GF --- tomcat_bec/devices/gigafrost/gigafrostcamera.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 5d1181f..d468237 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -7,7 +7,8 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ from time import sleep -from ophyd import Signal, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus +from ophyd import Signal, SignalRO, Component, EpicsSignal, EpicsSignalRO, Kind, DeviceStatus +from ophyd.device import DynamicDeviceComponent from ophyd_devices.interfaces.base_classes.psi_detector_base import ( CustomDetectorMixin, PSIDetectorBase, @@ -266,6 +267,13 @@ class GigaFrostCamera(PSIDetectorBase): cmdSetParam = Component(EpicsSignal, "SET_PARAM.PROC", put_complete=True, kind=Kind.omitted) cfgAcqMode = Component(EpicsSignal, "ACQMODE", put_complete=True, kind=Kind.config) + array_size = DynamicDeviceComponent({ + "array_size_x": (EpicsSignalRO, "ROIX", {'auto_monitor': True}), + "array_size_y": (EpicsSignalRO, "ROIY", {'auto_monitor': True}), + }, doc="Size of the array in the XY dimensions") + + + # UDP header cfgUdpNumPorts = Component(EpicsSignal, "PORTS", put_complete=True, kind=Kind.config) cfgUdpNumFrames = Component(EpicsSignal, "FRAMENUM", put_complete=True, kind=Kind.config) -- 2.49.1 From 0a1e11ed4f5c5d34797dab3232e2709cec758140 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Thu, 30 Jan 2025 17:19:43 +0100 Subject: [PATCH 03/14] Work on Helge cameras --- .../device_configs/microxas_test_bed.yaml | 13 + tomcat_bec/devices/gigafrost/helgecamera.py | 358 +++++++----------- 2 files changed, 157 insertions(+), 214 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index a759856..d9fe00f 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -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 diff --git a/tomcat_bec/devices/gigafrost/helgecamera.py b/tomcat_bec/devices/gigafrost/helgecamera.py index 28fd8e3..3f9751a 100644 --- a/tomcat_bec/devices/gigafrost/helgecamera.py +++ b/tomcat_bec/devices/gigafrost/helgecamera.py @@ -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) -- 2.49.1 From a78077139ac069f0ec102cf07d85beda9ac0b16a Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Fri, 31 Jan 2025 12:24:01 +0100 Subject: [PATCH 04/14] Basic PCO functionality tested --- .../{helgecamera.py => pcoedgecamera.py} | 67 +++++++++---------- 1 file changed, 32 insertions(+), 35 deletions(-) rename tomcat_bec/devices/gigafrost/{helgecamera.py => pcoedgecamera.py} (90%) diff --git a/tomcat_bec/devices/gigafrost/helgecamera.py b/tomcat_bec/devices/gigafrost/pcoedgecamera.py similarity index 90% rename from tomcat_bec/devices/gigafrost/helgecamera.py rename to tomcat_bec/devices/gigafrost/pcoedgecamera.py index 3f9751a..8225111 100644 --- a/tomcat_bec/devices/gigafrost/helgecamera.py +++ b/tomcat_bec/devices/gigafrost/pcoedgecamera.py @@ -5,11 +5,8 @@ Created on Wed Dec 6 11:33:54 2023 @author: mohacsi_i """ -from ophyd import Device, Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind -from ophyd.status import Status, SubscriptionStatus, StatusBase, DeviceStatus -from time import sleep -import warnings -import numpy as np +from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind +from ophyd.status import SubscriptionStatus 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 @@ -32,10 +29,10 @@ class PcoEdgeCameraMixin(CustomDeviceMixin): """ # Gigafrost can finish a run without explicit unstaging - if self.parent.infoBusyFlag.value: - logger.warning("Camera is already running, unstaging it first!") + 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() - sleep(0.5) + time.sleep(0.5) # Fish out our configuration from scaninfo (via explicit or generic addressing) scanparam = self.parent.scaninfo.scan_msg.info @@ -95,8 +92,6 @@ class HelgeCameraBase(PSIDeviceBase): 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 # ######################################################################## # General hardware info (in AD nomenclature) @@ -166,14 +161,9 @@ class HelgeCameraBase(PSIDeviceBase): def configure(self, d: dict = {}) -> tuple: """ Configure the base Helge camera device""" - if self.state in ["OFFLINE", "REMOVED", "RUNNING"]: + if self.state not in ("IDLE"): raise RuntimeError(f"Can't change configuration from state {self.state}") - # Stop acquisition - self.unstage() - if not self._initialized: - pass - # If Bluesky style configure if d is not None: # Commonly changed settings @@ -209,7 +199,7 @@ class HelgeCameraBase(PSIDeviceBase): # Subscribe and wait for update def isRunning(*args, old_value, value, timestamp, **kwargs): - return bool(self.state=="RUNNING") + return bool(value==6) status = SubscriptionStatus(self.camStatusCode, isRunning, timeout=5, settle_time=0.2) status.wait() @@ -222,7 +212,7 @@ class HelgeCameraBase(PSIDeviceBase): # 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 = SubscriptionStatus(self.camStatusCode, isIdle, timeout=5, settle_time=0.2) status.wait() @@ -265,8 +255,6 @@ class PcoEdgeBase(HelgeCameraBase): bufferRecMode = Component(EpicsSignalRO, "RECMODE", 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) - - def configure(self, d: dict = {}) -> tuple: """ @@ -276,9 +264,29 @@ class PcoEdgeBase(HelgeCameraBase): 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. - """ - old = self.read_configuration() - + + 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) @@ -300,20 +308,9 @@ class PcoEdgeBase(HelgeCameraBase): super().configure(d) - - - - - # Automatically connect to test camera if directly invoked if __name__ == "__main__": # Drive data collection - cam = HelgeCameraBase("X02DA-CCDCAM2:", name="mcpcam") + cam = PcoEdgeBase("X02DA-CCDCAM2:", name="mcpcam") cam.wait_for_connection() - - - - - - -- 2.49.1 From 3b599dc0629bd10d302d8fac0e11066eac0d00ed Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Tue, 4 Feb 2025 17:58:39 +0100 Subject: [PATCH 05/14] Fixed file_path, added file_prefix --- tomcat_bec/devices/gigafrost/stddaq_client.py | 37 +++++++++---- tomcat_bec/scans/tutorial_fly_scan.py | 1 - tomcat_bec/scripts/scans_fede.py | 52 ++++++++++++------- 3 files changed, 60 insertions(+), 30 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/stddaq_client.py b/tomcat_bec/devices/gigafrost/stddaq_client.py index 3d9af26..e9d51d9 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_client.py +++ b/tomcat_bec/devices/gigafrost/stddaq_client.py @@ -10,6 +10,7 @@ import json from time import sleep from threading import Thread import requests +import os from ophyd import Device, Signal, Component, Kind, Staged from ophyd.status import SubscriptionStatus @@ -20,6 +21,7 @@ from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError 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 bec_lib import bec_logger +from bec_lib.file_utils import FileWriter logger = bec_logger.logger @@ -47,7 +49,21 @@ class StdDaqMixin(CustomDeviceMixin): if 'nr_writers' in scanargs and scanargs['nr_writers'] != None: d['nr_writers'] = scanargs['nr_writers'] if 'file_path' in scanargs and scanargs['file_path']!=None: - self.parent.file_path.set(scanargs['file_path']).wait() + self.parent.file_path.set(scanargs['file_path'].replace('data','gpfs')).wait() + print(scanargs['file_path']) + if os.path.isdir(scanargs['file_path']): + print("isdir") + pass + else: + print("creating") + try: + os.makedirs(scanargs['file_path'], 0o777) + os.system('chmod -R 777 ' + scanargs['base_path']) + except: + print('Problem with creating folder') + if 'file_prefix' in scanargs and scanargs['file_prefix']!=None: + print(scanargs['file_prefix']) + self.parent.file_prefix.set(scanargs['file_prefix']).wait() if "daq_num_points" in scanargs: d["num_points_total"] = scanargs["daq_num_points"] @@ -138,7 +154,8 @@ class StdDaqClient(PSIDeviceBase): runstatus = Component(Signal, value="unknown", kind=Kind.normal, metadata={'write_access': False}) num_images = Component(Signal, value=10000, kind=Kind.config) file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config) - # Configuration attributes + file_prefix = Component(Signal, value="file", kind=Kind.config) + # Configuration attributes rest_url = Component(Signal, kind=Kind.config, metadata={'write_access': False}) cfg_detector_name = Component(Signal, kind=Kind.config) cfg_detector_type = Component(Signal, kind=Kind.config) @@ -245,6 +262,7 @@ class StdDaqClient(PSIDeviceBase): nr_writers: int, optional Number of writers [int]. """ + # Configuration parameters if 'image_width' in d and d['image_width']!=None: self.cfg_pixel_width.set(d['image_width']).wait() @@ -257,8 +275,6 @@ class StdDaqClient(PSIDeviceBase): # Run parameters if 'num_points_total' in d: self.num_images.set(d['num_points_total']).wait() - if 'file_path' in d and d['file_path']!=None: - self.file_path.set(d['file_path']).wait() # Restart the DAQ if resolution changed cfg = self.get_daq_config() @@ -288,10 +304,8 @@ class StdDaqClient(PSIDeviceBase): it for obvious failures. """ # Can't stage into a running exposure - print('Before') if self.state() != 'idle': raise RuntimeError(f"[{self.name}] stdDAQ can't stage from state: {self.state()}") - print('After') # Must make sure that image size matches the data source if self.data_source_name is not None: @@ -308,10 +322,12 @@ class StdDaqClient(PSIDeviceBase): file_path = self.file_path.get() num_images = self.num_images.get() + file_prefix = self.file_prefix.get() + print(file_prefix) # New connection self._wsclient = self.connect() - message = {"command": "start", "path": file_path, "n_image": num_images, } + message = {"command": "start", "path": file_path, "file_prefix": file_prefix, "n_image": num_images, } reply = self.message(message) if reply is not None: @@ -413,7 +429,7 @@ class StdDaqClient(PSIDeviceBase): if r.status_code != 200: raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") # Wait for service to restart (and connect to make sure) - sleep(settle_time) + #sleep(settle_time) self.connect() return r.json() @@ -423,16 +439,15 @@ class StdDaqClient(PSIDeviceBase): """ url = self.rest_url.get() + '/api/h5/create_interleaved_vds' file_path = self.file_path.get() + file_prefix = self.file_prefix.get() r = requests.post( url, params = {'user': 'ioc'}, - data = {'base_path': file_path, 'output_file': 'fede_virtual_test'}, + json = {'base_path': file_path, 'file_prefix': file_prefix, 'output_file': file_prefix.rstrip('_') + '.h5'}, timeout = 2, headers = {'Content-type': 'application/json'} ) - print(r) - print(file_path) def nuke(self, restarttime=5): """ Reconfigures the stdDAQ to restart the services. This causes diff --git a/tomcat_bec/scans/tutorial_fly_scan.py b/tomcat_bec/scans/tutorial_fly_scan.py index 9b946a7..b027af8 100644 --- a/tomcat_bec/scans/tutorial_fly_scan.py +++ b/tomcat_bec/scans/tutorial_fly_scan.py @@ -4,7 +4,6 @@ import numpy as np from bec_lib.device import DeviceBase from bec_server.scan_server.scans import Acquire, AsyncFlyScanBase - class AcquireDark(Acquire): scan_name = "acquire_dark" required_kwargs = ["exp_burst"] diff --git a/tomcat_bec/scripts/scans_fede.py b/tomcat_bec/scripts/scans_fede.py index f1a09eb..a02f5aa 100644 --- a/tomcat_bec/scripts/scans_fede.py +++ b/tomcat_bec/scripts/scans_fede.py @@ -26,13 +26,10 @@ class Measurement: """ Build and set filename for bec and stddaq """ - bec.system_config.file_suffix = self.sample_name - bec.system_config.file_directory = os.path.join(self.data_path,self.sample_name) - self.file_path = '/data/test/test-beamline/test_fede' - if os.path.isdir(self.file_path): - pass - else: - os.mkdir(self.file_path) + #bec.system_config.file_suffix = self.sample_name + self.scan_sample_name = 'S' + str(bec.queue.next_scan_number) + '_' + self.sample_name + print(self.scan_sample_name) + bec.system_config.file_directory = os.path.join(self.data_path,self.sample_name,self.scan_sample_name) def configure(self,sample_name=None, data_path=None, exposure_time=None, exposure_period=None, roix=None, roiy=None,nimages=None, @@ -84,9 +81,27 @@ class Measurement: self.roiy = roiy self.build_filename() + + def show_all(self): + + """ + Show configuration + + TODO: check active devices, write the config for each device and if none read from device + """ + + print("Sample name: " + self.sample_name) + print("Data path: " + self.data_path) + print("Number of images: " + str(self.nimages)) + print("Number of darks: " + str(self.nimages_dark)) + print("Number of flats: " + str(self.nimages_white)) + print("Exposure time: " + str(self.exposure_time)) + print("Exposure period: " + str(self.exposure_period)) + print("Roix: " + str(self.roix)) + print("Roiy: " + str(self.roiy)) def acquire_darks(self,nimages_dark, exposure_time=None, exposure_period=None, - roix=None, roiy=None, acq_mode=None, file_path=None): + roix=None, roiy=None, acq_mode=None): """ Acquire a set of dark images with shutters closed. @@ -104,8 +119,6 @@ class Measurement: ROI size in the y-direction [pixels] acq_mode : str, optional Predefined acquisition mode (default=None) - file_path : str, optional - File path for standard daq (default=None) Example: -------- @@ -150,14 +163,17 @@ class Measurement: self.roix = roix if roiy != None: self.roiy = roiy - - if file_path!=None: - if os.path.isdir(file_path): - pass - else: - os.mkdir(file_path) - + + # To add folder with device name + file_path = os.path.join('/data/test/test-beamline',bec.system_config.file_directory, '_device_dat') # _device_dat does not work for now. + print(file_path) + # A hack for now + base_path = os.path.join('/data/test/test-beamline',self.data_path) + print(base_path) + file_prefix = self.scan_sample_name + '_data_dark_' # to add device name + print(file_prefix) ### TODO: camera reset print("Handing over to 'scans.acquire_dark") scans.acquire_dark(exp_burst=nimages_dark, exp_time=self.exposure_time, exp_period=self.exposure_period, image_width=self.roix, - image_height=self.roiy, acq_mode=acq_mode, file_path=file_path, nr_writers=2) \ No newline at end of file + image_height=self.roiy, acq_mode=acq_mode, file_path=file_path, nr_writers=2, base_path=base_path, + file_prefix=file_prefix) \ No newline at end of file -- 2.49.1 From 801aa62f821ac64343d7af38a7b5b8059516c8f8 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Wed, 5 Feb 2025 16:47:33 +0100 Subject: [PATCH 06/14] Instructions for PCO edge --- tomcat_bec/devices/gigafrost/gigafrostcamera.py | 3 ++- tomcat_bec/devices/gigafrost/pcoedgecamera.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index d468237..09c40d5 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -194,7 +194,7 @@ class GigaFrostCameraMixin(CustomDetectorMixin): def on_unstage(self) -> None: """Specify actions to be executed during unstage. - This step should include checking if the acqusition was successful, + This step should include checking if the acquisition was successful, and publishing the file location and file event message, with flagged done to BEC. """ @@ -577,6 +577,7 @@ class GigaFrostCamera(PSIDetectorBase): self.cfgEnableScheme.set(0).wait() # Set modes + self.cmdSoftEnable.set(0).wait() self.enable_mode = "soft" self.trigger_mode = "auto" self.exposure_mode = "timer" diff --git a/tomcat_bec/devices/gigafrost/pcoedgecamera.py b/tomcat_bec/devices/gigafrost/pcoedgecamera.py index 8225111..cf7a34f 100644 --- a/tomcat_bec/devices/gigafrost/pcoedgecamera.py +++ b/tomcat_bec/devices/gigafrost/pcoedgecamera.py @@ -91,6 +91,20 @@ class HelgeCameraBase(PSIDeviceBase): 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 images and start file transfer with FTRANSFER. + The ZMQ connection streams out the data in PUSH-PULL mode, i.e. it needs incoming connection. + + STOREMODE sets the acquisition mode: + if STOREMODE == Recorder + Fills up the buffer with images and SAVESTART and SAVESTOP selects a ROI of images to be streamed + + if STOREMODE == FIFO buffer + Continously streams out data using the buffer as a FIFO queue and SAVESTART and SAVESTOP selects a ROI of images to be streamed continously (i.e. a large SAVESTOP streams indefinitely) + + """ # ######################################################################## -- 2.49.1 From 5c168a902d15c8145b40a41740133f8c57e0482f Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Wed, 5 Feb 2025 17:11:04 +0100 Subject: [PATCH 07/14] Acquisition dark working. --- .../devices/gigafrost/gigafrostcamera.py | 2 +- tomcat_bec/scripts/scans_fede.py | 58 ++++++++----------- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 09c40d5..186badf 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -577,7 +577,7 @@ class GigaFrostCamera(PSIDetectorBase): self.cfgEnableScheme.set(0).wait() # Set modes - self.cmdSoftEnable.set(0).wait() + #self.cmdSoftEnable.set(0).wait() self.enable_mode = "soft" self.trigger_mode = "auto" self.exposure_mode = "timer" diff --git a/tomcat_bec/scripts/scans_fede.py b/tomcat_bec/scripts/scans_fede.py index a02f5aa..dacfb6f 100644 --- a/tomcat_bec/scripts/scans_fede.py +++ b/tomcat_bec/scripts/scans_fede.py @@ -18,18 +18,33 @@ class Measurement: self.roix = None self.roiy = None - bec.system_config.file_suffix = self.sample_name + enabled_detectors = [obj for obj in dev.get_devices_with_tags('camera') if obj.enabled] + if len(enabled_detectors) != 1: + print('WARNING! More than 1 detector active') + self.det = enabled_detectors[0] + self.device_name = enabled_detectors[0].name + + bec.system_config.file_suffix = None bec.system_config.file_directory = os.path.join(self.data_path,self.sample_name) + self.build_filename() def build_filename(self): """ - Build and set filename for bec and stddaq + Build and set filepath for bec + Build filepath and file prefix for stddaq """ - #bec.system_config.file_suffix = self.sample_name + self.scan_sample_name = 'S' + str(bec.queue.next_scan_number) + '_' + self.sample_name - print(self.scan_sample_name) + + # File path for bec bec.system_config.file_directory = os.path.join(self.data_path,self.sample_name,self.scan_sample_name) + + # File path for stddaq (for now root folders different between console and stddaq server) + self.file_path = os.path.join('/data/test/test-beamline',bec.system_config.file_directory, '_device_dat', self.device_name) # _device_dat does not work for now. + # A hack for now to create the right permissions + self.base_path = os.path.join('/data/test/test-beamline',self.data_path) + self.file_prefix = self.scan_sample_name + '_' + self.device_name + '_data_dark_' def configure(self,sample_name=None, data_path=None, exposure_time=None, exposure_period=None, roix=None, roiy=None,nimages=None, @@ -134,27 +149,6 @@ class Measurement: dev.daq_stream0.enabled = True dev.daq_stream1.enabled = False - - dev.gfcam.cfgAcqMode.set(1).wait() - dev.gfcam.cmdSetParam.set(1).wait() - dev.gfcam.cfgEnableExt.set(0).wait() - dev.gfcam.cfgEnableSoft.set(0).wait() - dev.gfcam.cfgEnableAlways.set(1).wait() - - dev.gfcam.cfgTrigExt.set(0).wait() - dev.gfcam.cfgTrigSoft.set(0).wait() - dev.gfcam.cfgTrigTimer.set(0).wait() - dev.gfcam.cfgTrigAuto.set(1).wait() - - dev.gfcam.cfgExpExt.set(0).wait() - dev.gfcam.cfgExpSoft.set(0).wait() - dev.gfcam.cfgExpTimer.set(1).wait() - - dev.gfcam.cfgCntStartBit.set(0).wait() - - # Commit changes to GF - dev.gfcam.cmdSetParam.set(1).wait() - if exposure_time != None: self.exposure_time = exposure_time if exposure_period != None: @@ -164,16 +158,10 @@ class Measurement: if roiy != None: self.roiy = roiy - # To add folder with device name - file_path = os.path.join('/data/test/test-beamline',bec.system_config.file_directory, '_device_dat') # _device_dat does not work for now. - print(file_path) - # A hack for now - base_path = os.path.join('/data/test/test-beamline',self.data_path) - print(base_path) - file_prefix = self.scan_sample_name + '_data_dark_' # to add device name - print(file_prefix) + self.build_filename() + ### TODO: camera reset print("Handing over to 'scans.acquire_dark") scans.acquire_dark(exp_burst=nimages_dark, exp_time=self.exposure_time, exp_period=self.exposure_period, image_width=self.roix, - image_height=self.roiy, acq_mode=acq_mode, file_path=file_path, nr_writers=2, base_path=base_path, - file_prefix=file_prefix) \ No newline at end of file + image_height=self.roiy, acq_mode=acq_mode, file_path=self.file_path, nr_writers=2, base_path=self.base_path, + file_prefix=self.file_prefix) \ No newline at end of file -- 2.49.1 From 0dc6412ed85f66c3485a1070f2be58190e33dabd Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Wed, 5 Feb 2025 17:32:34 +0100 Subject: [PATCH 08/14] Dummy PCO edge consumer --- tomcat_bec/devices/gigafrost/pco_datasink.py | 192 ++++++++++++++++++ tomcat_bec/devices/gigafrost/pcoedgecamera.py | 2 + 2 files changed, 194 insertions(+) create mode 100644 tomcat_bec/devices/gigafrost/pco_datasink.py diff --git a/tomcat_bec/devices/gigafrost/pco_datasink.py b/tomcat_bec/devices/gigafrost/pco_datasink.py new file mode 100644 index 0000000..b76defb --- /dev/null +++ b/tomcat_bec/devices/gigafrost/pco_datasink.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +""" +Standard DAQ preview image stream module + +Created on Thu Jun 27 17:28:43 2024 + +@author: mohacsi_i +""" +import json +import enum +from time import sleep, time +from threading import Thread +import zmq +import numpy as np +from ophyd import Device, Signal, Component, Kind, DeviceStatus +from ophyd_devices.interfaces.base_classes.psi_detector_base import ( + CustomDetectorMixin, + PSIDetectorBase, +) + +from bec_lib import bec_logger +logger = bec_logger.logger +ZMQ_TOPIC_FILTER = b'' + + + +class PcoTestConsumerMixin(CustomDetectorMixin): + """Setup class for the standard DAQ preview stream + + Parent class: CustomDetectorMixin + """ + def on_stage(self): + """Start listening for preview data stream""" + if self.parent._mon is not None: + self.parent.unstage() + sleep(0.5) + + self.parent.connect() + self._stop_polling = False + self.parent._mon = Thread(target=self.poll, daemon=True) + self.parent._mon.start() + + def on_unstage(self): + """Stop a running preview""" + if self.parent._mon is not None: + self._stop_polling = True + # Might hang on recv_multipart + self.parent._mon.join(timeout=1) + # So also disconnect the socket + self.parent.disconnect() + + def on_stop(self): + """Stop a running preview""" + self.parent.disconnect() + + def poll(self): + """Collect streamed updates""" + try: + t_last = time() + while True: + try: + # Exit loop and finish monitoring + if self._stop_polling: + logger.info(f"[{self.parent.name}]\tDetaching monitor") + break + + # pylint: disable=no-member + r = self.parent._socket.recv() + + # Length and throtling checks + t_curr = time() + t_elapsed = t_curr - t_last + if t_elapsed < self.parent.throttle.get(): + continue + """ + # Unpack the Array V1 reply to metadata and array data + meta, data = r + print(meta) + + # Update image and update subscribers + header = json.loads(meta) + if header["type"] == "uint16": + image = np.frombuffer(data, dtype=np.uint16) + if image.size != np.prod(header['shape']): + err = f"Unexpected array size of {image.size} for header: {header}" + raise ValueError(err) + image = image.reshape(header['shape']) + + # Update image and update subscribers + self.parent.frame.put(header['frame'], force=True) + self.parent.image_shape.put(header['shape'], force=True) + self.parent.image.put(image, force=True) + self.parent._last_image = image + self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image) + """ + t_last = t_curr + #logger.info( + # f"[{self.parent.name}] Updated frame {header['frame']}\t" + # f"Shape: {header['shape']}\tMean: {np.mean(image):.3f}" + # ) + except ValueError: + # Happens when ZMQ partially delivers the multipart message + pass + except zmq.error.Again: + # Happens when receive queue is empty + sleep(0.1) + except Exception as ex: + logger.info(f"[{self.parent.name}]\t{str(ex)}") + raise + finally: + try: + self.parent._socket.disconnect() + except: + pass + self.parent._mon = None + logger.info(f"[{self.parent.name}]\tDetaching monitor") + + +class PcoTestConsumer(PSIDetectorBase): + """Detector wrapper class around the StdDaq preview image stream. + + This was meant to provide live image stream directly from the StdDAQ. + Note that the preview stream must be already throtled in order to cope + with the incoming data and the python class might throttle it further. + + You can add a preview widget to the dock by: + cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1') + """ + # Subscriptions for plotting image + USER_ACCESS = ["get_last_image"] + SUB_MONITOR = "device_monitor_2d" + _default_sub = SUB_MONITOR + + custom_prepare_cls = PcoTestConsumerMixin + + # Status attributes + url = Component(Signal, kind=Kind.config) + throttle = Component(Signal, value=0.25, kind=Kind.config) + frame = Component(Signal, kind=Kind.hinted) + image_shape = Component(Signal, kind=Kind.normal) + # FIXME: The BEC client caches the read()s from the last 50 scans + image = Component(Signal, kind=Kind.omitted) + _last_image = None + _mon = None + _socket = None + + def __init__( + self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs + ) -> None: + super().__init__(*args, parent=parent, **kwargs) + self.url._metadata["write_access"] = False + self.image._metadata["write_access"] = False + self.frame._metadata["write_access"] = False + self.image_shape._metadata["write_access"] = False + self.url.set(url, force=True).wait() + + def connect(self): + """Connect to te StDAQs PUB-SUB streaming interface + + StdDAQ may reject connection for a few seconds when it restarts, + so if it fails, wait a bit and try to connect again. + """ + # pylint: disable=no-member + # Socket to talk to server + context = zmq.Context() + self._socket = context.socket(zmq.PULL) + try: + self._socket.connect(self.url.get()) + except ConnectionRefusedError: + sleep(1) + self._socket.connect(self.url.get()) + + def disconnect(self): + """Disconnect + """ + try: + if self._socket is not None: + self._socket.disconnect(self.url.get()) + except zmq.ZMQError: + pass + finally: + self._socket = None + + + def get_image(self): + return self._last_image + + +# Automatically connect to MicroSAXS testbench if directly invoked +if __name__ == "__main__": + daq = PcoTestConsumerMixin(url="tcp://129.129.106.124:8080", name="preview") + daq.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/pcoedgecamera.py b/tomcat_bec/devices/gigafrost/pcoedgecamera.py index cf7a34f..edbfa5e 100644 --- a/tomcat_bec/devices/gigafrost/pcoedgecamera.py +++ b/tomcat_bec/devices/gigafrost/pcoedgecamera.py @@ -104,6 +104,8 @@ class HelgeCameraBase(PSIDeviceBase): if STOREMODE == FIFO buffer Continously streams out data using the buffer as a FIFO queue and SAVESTART and SAVESTOP selects a ROI of images 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 + """ -- 2.49.1 From c729d67763a409f8766d2a0f39accf1e99da03ea Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Mon, 10 Feb 2025 16:41:31 +0100 Subject: [PATCH 09/14] Added flat acquisition --- .../device_configs/microxas_test_bed.yaml | 38 ++--- tomcat_bec/scans/__init__.py | 2 +- tomcat_bec/scans/tutorial_fly_scan.py | 57 ++++--- tomcat_bec/scripts/scans_fede.py | 151 +++++++++++++++--- 4 files changed, 184 insertions(+), 64 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index d9fe00f..d364070 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -10,30 +10,20 @@ eyex: enabled: true readOnly: false softwareTrigger: false -# eyey: -# readoutPriority: baseline -# description: X-ray eye axis Y -# deviceClass: tomcat_bec.devices.psimotor.EpicsMotorEC -# deviceConfig: -# prefix: MTEST-X05LA-ES2-XRAYEYE:M2 -# deviceTags: -# - xray-eye -# onFailure: buffer -# enabled: true -# readOnly: false -# softwareTrigger: false -# eyez: -# readoutPriority: baseline -# description: X-ray eye axis Z -# deviceClass: tomcat_bec.devices.psimotor.EpicsMotorEC -# deviceConfig: -# prefix: MTEST-X05LA-ES2-XRAYEYE:M3 -# deviceTags: -# - xray-eye -# onFailure: buffer -# enabled: true -# readOnly: false -# softwareTrigger: false + +eyez: + readoutPriority: baseline + description: X-ray eye axis Z + deviceClass: tomcat_bec.devices.psimotor.EpicsMotorEC + deviceConfig: + prefix: MTEST-X05LA-ES2-XRAYEYE:M3 + deviceTags: + - xray-eye + onFailure: buffer + enabled: true + readOnly: false + softwareTrigger: false + femto_mean_curr: readoutPriority: monitored description: Femto mean current diff --git a/tomcat_bec/scans/__init__.py b/tomcat_bec/scans/__init__.py index 2eb3866..230e33e 100644 --- a/tomcat_bec/scans/__init__.py +++ b/tomcat_bec/scans/__init__.py @@ -1,2 +1,2 @@ -from .tutorial_fly_scan import AcquireDark, AcquireFlat, TutorialFlyScanContLine +from .tutorial_fly_scan import AcquireDark, AcquireWhite, TutorialFlyScanContLine from .tomcat_scans import TomcatStepScan, TomcatSnapNStep, TomcatSimpleSequence diff --git a/tomcat_bec/scans/tutorial_fly_scan.py b/tomcat_bec/scans/tutorial_fly_scan.py index b027af8..8470680 100644 --- a/tomcat_bec/scans/tutorial_fly_scan.py +++ b/tomcat_bec/scans/tutorial_fly_scan.py @@ -27,7 +27,7 @@ class AcquireDark(Acquire): image_height : int, optional ROI size in the y-direction [pixels] acq_mode : str, optional - Predefined acquisition mode (default=) + Predefined acquisition mode (default= 'default') file_path : str, optional File path for standard daq @@ -51,39 +51,58 @@ class AcquireDark(Acquire): yield from super().scan_core() -class AcquireFlat(Acquire): - scan_name = "acquire_flat" - required_kwargs = ["num"] - gui_config = {"Acquisition parameters": ["num"]} +class AcquireWhite(Acquire): + scan_name = "acquire_white" + required_kwargs = ["exp_burst", "sample_position_out", "sample_angle_out"] + gui_config = {"Acquisition parameters": ["exp_burst"]} - def __init__(self, num: int, out_position: float, **kwargs): + def __init__(self, exp_burst: int, sample_position_out: float, sample_angle_out: float, **kwargs): """ - Acquire a flat field image. This scan is used to acquire a flat field image. The flat field image is an image taken - with the shutter open but the sample out of the beam. The flat field image is used to correct the data images for + Acquire flat field images. This scan is used to acquire flat field images. The flat field image is an image taken + with the shutter open but the sample out of the beam. Flat field images are used to correct the data images for non-uniformity in the detector. Args: - num (int): number of flat field images to acquire - out_position (float): position to move the sample stage to take the flat field image + exp_burst : int + Number of flat field images to acquire (no default) + sample_position_out : float + Position to move the sample stage to position the sample out of beam and take flat field images + sample_angle_out : float + Angular position where to take the flat field images + exp_time : float, optional + Exposure time [ms]. If not specified, the currently configured value on the camera will be used + exp_period : float, optional + Exposure period [ms]. If not specified, the currently configured value on the camera will be used + image_width : int, optional + ROI size in the x-direction [pixels]. If not specified, the currently configured value on the camera will be used + image_height : int, optional + ROI size in the y-direction [pixels]. If not specified, the currently configured value on the camera will be used + acq_mode : str, optional + Predefined acquisition mode (default= 'default') + file_path : str, optional + File path for standard daq Returns: ScanReport Examples: - >>> scans.acquire_flat(5, 20) + >>> scans.acquire_white(5, 20) """ super().__init__(**kwargs) - self.burst_at_each_point = num - self.out_position = out_position - self.sample_stage = "samy" # change to the correct sample stage device - self.shutter = "hx" # change to the correct shutter device + self.burst_at_each_point = 1 + self.sample_position_out = sample_position_out + self.sample_angle_out = sample_angle_out + + self.scan_motors = ["eyex", "eyez", "es1_roty"] # change to the correct shutter device +# self.scan_motors = ["eyex", "eyez"] # change to the correct shutter device + self.dark_shutter_pos = 1 ### change with a variable + def scan_core(self): # open the shutter and move the sample stage to the out position - yield from self.stubs.set_and_wait( - device=[self.shutter, self.sample_stage], positions=[1, self.out_position] - ) + yield from self._move_scan_motors_and_wait([self.dark_shutter_pos, self.sample_position_out, self.sample_angle_out]) +# yield from self._move_scan_motors_and_wait([self.dark_shutter_pos, self.sample_position_out]) yield from super().scan_core() @@ -168,7 +187,7 @@ class TutorialFlyScanContLine(AsyncFlyScanBase): show_asap=True, rid=self.metadata.get("RID"), ) - flats = AcquireFlat( + flats = AcquireWhite( num=self.num_flats, exp_time=self.exp_time, out_position=self.sample_out, diff --git a/tomcat_bec/scripts/scans_fede.py b/tomcat_bec/scripts/scans_fede.py index dacfb6f..917194d 100644 --- a/tomcat_bec/scripts/scans_fede.py +++ b/tomcat_bec/scripts/scans_fede.py @@ -12,6 +12,11 @@ class Measurement: self.nimages_dark = 50 self.nimages_white = 100 + self.start_angle = 0 + self.sample_angle_out = 0 + self.sample_position_in = 0 + self.sample_position_out = 1 + # To be able to keep what is already set on the camera self.exposure_time = None self.exposure_period = None @@ -29,12 +34,20 @@ class Measurement: self.build_filename() - def build_filename(self): + def build_filename(self, acquisition_type='data'): """ Build and set filepath for bec Build filepath and file prefix for stddaq + + acquisition_type : string, optional + Type of images: a choice between dark, white, or data (default = data) """ + if acquisition_type != "data" and acquisition_type != "dark" and acquisition_type != "white": + print("WARNING: chosen acquisition type not permitted! \n") + print("The chosen acquitisition type as been set to \"data\"!") + acquisition_type == "data" + self.scan_sample_name = 'S' + str(bec.queue.next_scan_number) + '_' + self.sample_name # File path for bec @@ -44,11 +57,13 @@ class Measurement: self.file_path = os.path.join('/data/test/test-beamline',bec.system_config.file_directory, '_device_dat', self.device_name) # _device_dat does not work for now. # A hack for now to create the right permissions self.base_path = os.path.join('/data/test/test-beamline',self.data_path) - self.file_prefix = self.scan_sample_name + '_' + self.device_name + '_data_dark_' + self.file_prefix = self.scan_sample_name + '_' + self.device_name + '_' + acquisition_type + '_' def configure(self,sample_name=None, data_path=None, exposure_time=None, exposure_period=None, roix=None, roiy=None,nimages=None, - nimages_dark=None, nimages_white=None): + nimages_dark=None, nimages_white=None, + start_angle=None, sample_angle_out=None, + sample_position_in=None, sample_position_out=None): """ Reconfigure the measurement with any number of new parameter @@ -56,24 +71,34 @@ class Measurement: ---------- sample_name : string, optional Name of the sample or measurement. This name will be used to construct - the name of the measurement directory (default=None) + the name of the measurement directory (default = None) data_path : string, optional Information used to build the data directory for the measurement - (default=None) + (default = None) exposure_time : float, optional - Exposure time [ms] (default=None) + Exposure time [ms] (default = None) exposure_period : float, optional - Exposure period [ms] (default=None) + Exposure period [ms] (default = None) roix : int, optional - ROI size in the x-direction [pixels] (default=None) + ROI size in the x-direction [pixels] (default = None) roiy : int, optional - ROI size in the y-direction [pixels] (default=None) + ROI size in the y-direction [pixels] (default = None) nimages : int, optional - Number of images to acquire (default=None) + Number of images to acquire (default = None) nimages_dark : int, optional - Number of dark images to acquire (default=None) + Number of dark images to acquire (default = None) nimages_white : int, optional - Number of white images to acquire (default=None) + Number of white images to acquire (default = None) + start_angle : float, optional + The start angle for the scan [deg] (default = None) + sample_angle_out : float, optional + Sample rotation angle for sample out of the beam [deg] + (default = None) + sample_position_in : float, optional + Sample stage X position for sample in beam [um] (default = None) + sample_position_out : float, optional + Sample stage X position for sample out of the beam [um] + (default = None) """ if sample_name != None: @@ -94,6 +119,14 @@ class Measurement: self.roix = roix if roiy != None: self.roiy = roiy + if start_angle != None: + self.start_angle = start_angle + if sample_angle_out != None: + self.sample_angle_out = sample_angle_out + if sample_position_in != None: + self.sample_position_in = sample_position_in + if sample_position_out != None: + self.sample_position_out = sample_position_out self.build_filename() @@ -102,7 +135,7 @@ class Measurement: """ Show configuration - TODO: check active devices, write the config for each device and if none read from device + TODO: make it work for multiple devices """ print("Sample name: " + self.sample_name) @@ -110,11 +143,28 @@ class Measurement: print("Number of images: " + str(self.nimages)) print("Number of darks: " + str(self.nimages_dark)) print("Number of flats: " + str(self.nimages_white)) - print("Exposure time: " + str(self.exposure_time)) - print("Exposure period: " + str(self.exposure_period)) - print("Roix: " + str(self.roix)) - print("Roiy: " + str(self.roiy)) + if self.exposure_time == None: + print("Exposure time: " + str(self.det.cfgExposure.get())) + else: + print("Exposure time: " + str(self.exposure_time)) + if self.exposure_period == None: + print("Exposure peirod: " + str(self.det.cfgFramerate.get())) + else: + print("Exposure period: " + str(self.exposure_period)) + if self.roix == None: + print("Roix: " + str(self.det.cfgRoiX.get())) + else: + print("Roix: " + str(self.roix)) + if self.roiy == None: + print("Roiy: " + str(self.det.cfgRoiY.get())) + else: + print("Roiy: " + str(self.roiy)) + print("Start angle: " + str(self.start_angle)) + print("Sample angle out: " + str(self.sample_angle_out)) + print("Sample position in: " + str(self.sample_position_in)) + print("Sample position out: " + str(self.sample_position_out)) + def acquire_darks(self,nimages_dark, exposure_time=None, exposure_period=None, roix=None, roiy=None, acq_mode=None): """ @@ -137,7 +187,7 @@ class Measurement: Example: -------- - fede_darks(100, exposure_time=5) + m.acquire_darks(100, exposure_time=5) """ # dev.es1_tasks.enabled = False # dev.es1_psod.enabled = False @@ -158,10 +208,71 @@ class Measurement: if roiy != None: self.roiy = roiy - self.build_filename() + self.build_filename(acquisition_type='dark') ### TODO: camera reset print("Handing over to 'scans.acquire_dark") scans.acquire_dark(exp_burst=nimages_dark, exp_time=self.exposure_time, exp_period=self.exposure_period, image_width=self.roix, image_height=self.roiy, acq_mode=acq_mode, file_path=self.file_path, nr_writers=2, base_path=self.base_path, - file_prefix=self.file_prefix) \ No newline at end of file + file_prefix=self.file_prefix) + + def acquire_whites(self,nimages_whites, sample_angle_out=None, sample_position_out=None, + exposure_time=None, exposure_period=None, + roix=None, roiy=None, acq_mode=None): + """ + Acquire a set of whites images with shutters open and sample out of beam. + + Parameters + ---------- + nimages_whites : int + Number of white images to acquire (no default) + sample_angle_out : float, optional + Sample rotation angle for sample out of the beam [deg] + sample_position_out : float, optional + Sample stage X position for sample out of the beam [um] + exposure_time : float, optional + Exposure time [ms]. If not specified, the currently configured value on the camera will be used + exposure_period : float, optional + Exposure period [ms] + roix : int, optional + ROI size in the x-direction [pixels] + roiy : int, optional + ROI size in the y-direction [pixels] + acq_mode : str, optional + Predefined acquisition mode (default=None) + + Example: + -------- + m.acquire_whites(100, exposure_time=5) + """ + # dev.es1_tasks.enabled = False + # dev.es1_psod.enabled = False + # dev.es1_ddaq.enabled = False + # dev.es1_ismc.enabled = False + # dev.es1_roty.enabled = False + dev.gfcam.enabled = True + dev.gfdaq.enabled = True + dev.daq_stream0.enabled = True + dev.daq_stream1.enabled = False + + if sample_angle_out != None: + self.sample_angle_out = sample_angle_out + if sample_position_out != None: + self.sample_position_out = sample_position_out + if exposure_time != None: + self.exposure_time = exposure_time + if exposure_period != None: + self.exposure_period = exposure_period + if roix != None: + self.roix = roix + if roiy != None: + self.roiy = roiy + + self.build_filename(acquisition_type='white') + + ### TODO: camera reset + print("Handing over to 'scans.acquire_whites") + scans.acquire_white(exp_burst=nimages_whites, sample_angle_out=self.sample_angle_out, sample_position_out= self.sample_position_out, + exp_time=self.exposure_time, exp_period=self.exposure_period, image_width=self.roix, + image_height=self.roiy, acq_mode=acq_mode, file_path=self.file_path, nr_writers=2, base_path=self.base_path, + file_prefix=self.file_prefix) \ No newline at end of file -- 2.49.1 From a36c8237da866ec9b030bafa6c1ceef8df6286af Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Mon, 10 Feb 2025 18:18:08 +0100 Subject: [PATCH 10/14] PCO Edge in regular beamline config --- .../device_configs/microxas_test_bed.yaml | 38 +++++++- tomcat_bec/devices/__init__.py | 2 + tomcat_bec/devices/gigafrost/pcoedgecamera.py | 97 ++++++++++++++----- 3 files changed, 110 insertions(+), 27 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index d364070..8a98eee 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -176,15 +176,43 @@ daq_stream1: readoutPriority: monitored softwareTrigger: false -pco_stream0: - description: Raw camera stream from PCO.edge - deviceClass: tomcat_bec.devices.StdDaqPreviewDetector + +pcocam: + description: PCO.edge camera client + deviceClass: tomcat_bec.devices.PcoEdge5M deviceConfig: - url: 'tcp://129.129.106.124:8080' + prefix: 'X02DA-CCDCAM2:' + deviceTags: + - camera + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: false + +pcodaq: + description: GigaFrost stdDAQ client + deviceClass: tomcat_bec.devices.StdDaqClient + deviceConfig: + ws_url: 'ws://129.129.95.111:8081' + rest_url: 'http://129.129.95.111:5010' deviceTags: - std-daq enabled: true onFailure: buffer readOnly: false - readoutPriority: async + readoutPriority: monitored + softwareTrigger: false + +pco_stream0: + description: stdDAQ preview (2 every 555) + deviceClass: tomcat_bec.devices.StdDaqPreviewDetector + deviceConfig: + url: 'tcp://129.129.95.111:20010' + deviceTags: + - std-daq + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored softwareTrigger: false diff --git a/tomcat_bec/devices/__init__.py b/tomcat_bec/devices/__init__.py index ead4971..a9d2c1d 100644 --- a/tomcat_bec/devices/__init__.py +++ b/tomcat_bec/devices/__init__.py @@ -11,5 +11,7 @@ from .grashopper_tomcat import GrashopperTOMCAT from .psimotor import EpicsMotorMR, EpicsMotorEC from .gigafrost.gigafrostcamera import GigaFrostCamera +from .gigafrost.pcoedgecamera import PcoEdge5M + from .gigafrost.stddaq_client import StdDaqClient from .gigafrost.stddaq_preview import StdDaqPreviewDetector diff --git a/tomcat_bec/devices/gigafrost/pcoedgecamera.py b/tomcat_bec/devices/gigafrost/pcoedgecamera.py index edbfa5e..ed095eb 100644 --- a/tomcat_bec/devices/gigafrost/pcoedgecamera.py +++ b/tomcat_bec/devices/gigafrost/pcoedgecamera.py @@ -28,7 +28,7 @@ class PcoEdgeCameraMixin(CustomDeviceMixin): """Configure and arm PCO.Edge camera for acquisition """ - # Gigafrost can finish a run without explicit unstaging + # 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() @@ -36,25 +36,30 @@ class PcoEdgeCameraMixin(CustomDeviceMixin): # 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: + if 'num_images_total' in scanargs and scanargs['num_images_total'] is not None: + d['images_total'] = scanargs['num_images_total'] + 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']!=None: + 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']!=None: + 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']!=None: + 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']!=None: + # 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']!=None: + # 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: @@ -136,6 +141,24 @@ class HelgeCameraBase(PSIDeviceBase): 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) + + # ######################################################################## + # 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) + # ######################################################################## # Configuration state maschine with separate transition states camStatusCode = Component(EpicsSignalRO, "STATUSCODE", auto_monitor=True, kind=Kind.config) @@ -176,19 +199,43 @@ class HelgeCameraBase(PSIDeviceBase): raise ReadOnlyError("State is a ReadOnly property") def configure(self, d: dict = {}) -> tuple: - """ Configure the base Helge camera device""" + """ Configure the base Helge camera device + + Parameters as 'd' dictionary + ---------------------------- + num_images : int + Number of images to be taken during each scan. Meaning depends on + store mode. + exposure_time_ms : float + Exposure time [ms], usually gets set back to 20 ms + exposure_period_ms : float + Exposure period [ms], up to 200 ms. + store_mode : str + Buffer operation mode + *'Recorder' to record in buffer + *'FIFO buffer' for continous streaming + data_format : str + Usually set to 'ZEROMQ' + """ if self.state not in ("IDLE"): raise RuntimeError(f"Can't change configuration from state {self.state}") # If Bluesky style configure if d is not None: # Commonly changed settings + if 'num_images' in d: + self.file_savestop.set(d['num_images']).wait() 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() - + if 'exposure_period_ms' in d: + self.acquire_delay.set(d['exposure_period_ms']).wait() + if 'store_mode' in d: + self.bufferStoreMode.set(d['store_mode']).wait() + if 'data_format' in d: + self.file_format.set(d['data_format']).wait() + # State machine # Initial: BUSY and SET both low # 0. Write 1 to SET_PARAM @@ -231,17 +278,29 @@ class HelgeCameraBase(PSIDeviceBase): status = SubscriptionStatus(self.camStatusCode, isIdle, timeout=5, settle_time=0.2) status.wait() + # Data streaming is stopped by setting the max index to 0 + self.file_savestop.set(0).wait() -class PcoEdgeBase(HelgeCameraBase): + + def bluekickoff(self): + """ 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"] + USER_ACCESS = ["bluestage", "blueunstage", "bluekickoff"] # ######################################################################## # Additional status info @@ -266,12 +325,6 @@ class PcoEdgeBase(HelgeCameraBase): 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) - # ######################################################################## - # Buffer configuration - bufferRecMode = Component(EpicsSignalRO, "RECMODE", 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) - def configure(self, d: dict = {}) -> tuple: """ Camera configuration instructions: @@ -302,7 +355,7 @@ class PcoEdgeBase(HelgeCameraBase): 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) -- 2.49.1 From 6db73b366b48a3c446a25bab7798ef32677bf7c7 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Tue, 11 Feb 2025 17:12:21 +0100 Subject: [PATCH 11/14] WIP acquire_refs --- tomcat_bec/scans/__init__.py | 2 +- tomcat_bec/scans/tutorial_fly_scan.py | 200 ++++++++++++++++++++- tomcat_bec/scripts/scans_fede.py | 246 +++++++++++++++++++++----- 3 files changed, 402 insertions(+), 46 deletions(-) diff --git a/tomcat_bec/scans/__init__.py b/tomcat_bec/scans/__init__.py index 230e33e..ab461c9 100644 --- a/tomcat_bec/scans/__init__.py +++ b/tomcat_bec/scans/__init__.py @@ -1,2 +1,2 @@ -from .tutorial_fly_scan import AcquireDark, AcquireWhite, TutorialFlyScanContLine +from .tutorial_fly_scan import AcquireDark, AcquireWhite, AcquireRefs, TutorialFlyScanContLine from .tomcat_scans import TomcatStepScan, TomcatSnapNStep, TomcatSimpleSequence diff --git a/tomcat_bec/scans/tutorial_fly_scan.py b/tomcat_bec/scans/tutorial_fly_scan.py index 8470680..fe0ea53 100644 --- a/tomcat_bec/scans/tutorial_fly_scan.py +++ b/tomcat_bec/scans/tutorial_fly_scan.py @@ -46,7 +46,7 @@ class AcquireDark(Acquire): def scan_core(self): # close the shutter - yield from self._move_scan_motors_and_wait(self.dark_shutter_pos) +# yield from self._move_scan_motors_and_wait(self.dark_shutter_pos) #yield from self.stubs.set_and_wait(device=[self.shutter], positions=[0]) yield from super().scan_core() @@ -95,15 +95,205 @@ class AcquireWhite(Acquire): self.sample_angle_out = sample_angle_out self.scan_motors = ["eyex", "eyez", "es1_roty"] # change to the correct shutter device -# self.scan_motors = ["eyex", "eyez"] # change to the correct shutter device - self.dark_shutter_pos = 1 ### change with a variable + self.dark_shutter_pos_out = 1 ### change with a variable + self.dark_shutter_pos_in = 0 ### change with a variable def scan_core(self): # open the shutter and move the sample stage to the out position - yield from self._move_scan_motors_and_wait([self.dark_shutter_pos, self.sample_position_out, self.sample_angle_out]) -# yield from self._move_scan_motors_and_wait([self.dark_shutter_pos, self.sample_position_out]) + self.scan_motors = ["eyez", "es1_roty"] # change to the correct shutter device + yield from self._move_scan_motors_and_wait([self.sample_position_out, self.sample_angle_out]) + self.scan_motors = ["eyex"] # change to the correct shutter device + yield from self._move_scan_motors_and_wait([self.dark_shutter_pos_out]) + # TODO add opening of fast shutter yield from super().scan_core() + + # TODO add closing of fast shutter + yield from self._move_scan_motors_and_wait([self.dark_shutter_pos_in]) + +class AcquireProjectins(Acquire): + scan_name = "acquire_projections" + required_kwargs = ["exp_burst", "sample_position_in", "start_position", "angular_range"] + gui_config = {"Acquisition parameters": ["exp_burst"]} + + def __init__(self, + exp_burst: int, + sample_position_in: float, + start_position: float, + angular_range: float, + **kwargs): + """ + Acquire projection images. + + Args: + exp_burst : int + Number of flat field images to acquire (no default) + sample_position_in : float + Position to move the sample stage to position the sample in the beam + start_position : float + Angular start position for the scan + angular_range : float + Angular range + exp_time : float, optional + Exposure time [ms]. If not specified, the currently configured value on the camera will be used + exp_period : float, optional + Exposure period [ms]. If not specified, the currently configured value on the camera will be used + image_width : int, optional + ROI size in the x-direction [pixels]. If not specified, the currently configured value on the camera will be used + image_height : int, optional + ROI size in the y-direction [pixels]. If not specified, the currently configured value on the camera will be used + acq_mode : str, optional + Predefined acquisition mode (default= 'default') + file_path : str, optional + File path for standard daq + + Returns: + ScanReport + + Examples: + >>> scans.acquire_white(5, 20) + + """ + super().__init__(**kwargs) + self.burst_at_each_point = 1 + self.sample_position_in = sample_position_in + self.start_position = start_position + self.angular_range = angular_range + + self.scan_motors = ["eyex", "eyez", "es1_roty"] # change to the correct shutter device + self.dark_shutter_pos_out = 1 ### change with a variable + self.dark_shutter_pos_in = 0 ### change with a variable + + + def scan_core(self): + # open the shutter and move the sample stage to the out position + self.scan_motors = ["eyez", "es1_roty"] # change to the correct shutter device + yield from self._move_scan_motors_and_wait([self.sample_position_out, self.sample_angle_out]) + self.scan_motors = ["eyex"] # change to the correct shutter device + yield from self._move_scan_motors_and_wait([self.dark_shutter_pos_out]) + # TODO add opening of fast shutter + yield from super().scan_core() + + # TODO add closing of fast shutter + yield from self._move_scan_motors_and_wait([self.dark_shutter_pos_in]) + + +class AcquireRefs(Acquire): + scan_name = "acquire_refs" + required_kwargs = [] + gui_config = {} + + def __init__( + self, + num_darks: int = 0, + num_flats: int = 0, + sample_angle_out: float = 0, + sample_position_in: float = 0, + sample_position_out: float = 5000, + file_prefix_dark: str = 'tmp_dark', + file_prefix_white: str = 'tmp_white', + exp_time: float = 0, + exp_period: float = 0, + image_width: int = 2016, + image_height: int = 2016, + acq_mode: str = 'default', + file_path: str = 'tmp', + nr_writers: int = 2, + base_path: str = 'tmp', + **kwargs + ): + """ + Acquire reference images (darks + whites) and return to beam position. + + Reference images are acquired automatically in an optimized sequence and + the sample is returned to the sample_in_position afterwards. + + Args: + num_darks : int , optional + Number of dark field images to acquire + num_flats : int , optional + Number of white field images to acquire + sample_position_in : float , optional + Sample stage X position for sample in beam [um] + sample_position_out : float ,optional + Sample stage X position for sample out of the beam [um] + sample_angle_out : float , optional + Angular position where to take the flat field images + exp_time : float, optional + Exposure time [ms]. If not specified, the currently configured value on the camera will be used + exp_period : float, optional + Exposure period [ms]. If not specified, the currently configured value on the camera will be used + image_width : int, optional + ROI size in the x-direction [pixels]. If not specified, the currently configured value on the camera will be used + image_height : int, optional + ROI size in the y-direction [pixels]. If not specified, the currently configured value on the camera will be used + acq_mode : str, optional + Predefined acquisition mode (default= 'default') + file_path : str, optional + File path for standard daq + + Returns: + ScanReport + + Examples: + >>> scans.acquire_refs(sample_angle_out=90, sample_position_in=10, num_darks=5, num_flats=5, exp_time=0.1) + + """ + super().__init__(**kwargs) + self.sample_position_in = sample_position_in + self.sample_position_out = sample_position_out + self.sample_angle_out = sample_angle_out + self.num_darks = num_darks + self.num_flats = num_flats + self.file_prefix_dark = file_prefix_dark + self.file_prefix_white = file_prefix_white + self.exp_time = exp_time, + self.exp_period = exp_period, + self.image_width = image_width, + self.image_height = image_height, + self.acq_mode = acq_mode, + self.file_path = file_path, + self.nr_writers = nr_writers, + self.base_path = base_path, + + def scan_core(self): + + ## TODO move sample in position and do not wait + ## TODO move angle in position and do not wait + if self.num_darks: + self.connector.send_client_info( + f"Acquiring {self.num_darks} dark images", + show_asap=True, + rid=self.metadata.get("RID"), + ) + darks = AcquireDark( + exp_burst=self.num_darks, + file_prefix=self.file_prefix_dark, + device_manager=self.device_manager, + metadata=self.metadata + ) + yield from darks.scan_core() + self.point_id = darks.point_id + + if self.num_flats: + self.connector.send_client_info( + f"Acquiring {self.num_flats} flat field images", + show_asap=True, + rid=self.metadata.get("RID"), + ) + flats = AcquireWhite( + exp_burst=self.num_flats, + sample_position_out=self.sample_position_out, + sample_angle_out=self.sample_angle_out, + file_prefix=self.file_prefix_white, + device_manager=self.device_manager, + metadata=self.metadata, + ) + flats.point_id = self.point_id + yield from flats.scan_core() + self.point_id = flats.point_id + ## TODO move sample in beam and do not wait + ## TODO move rotation to angle and do not wait class TutorialFlyScanContLine(AsyncFlyScanBase): diff --git a/tomcat_bec/scripts/scans_fede.py b/tomcat_bec/scripts/scans_fede.py index 917194d..a4ce07b 100644 --- a/tomcat_bec/scripts/scans_fede.py +++ b/tomcat_bec/scripts/scans_fede.py @@ -8,6 +8,9 @@ class Measurement: def __init__(self): self.sample_name = 'tmp' self.data_path = 'disk_test' + bec.system_config.file_suffix = None + bec.system_config.file_directory = os.path.join(self.data_path,self.sample_name) + self.nimages = 1000 self.nimages_dark = 50 self.nimages_white = 100 @@ -23,16 +26,24 @@ class Measurement: self.roix = None self.roiy = None - enabled_detectors = [obj for obj in dev.get_devices_with_tags('camera') if obj.enabled] - if len(enabled_detectors) != 1: - print('WARNING! More than 1 detector active') - self.det = enabled_detectors[0] - self.device_name = enabled_detectors[0].name - - bec.system_config.file_suffix = None - bec.system_config.file_directory = os.path.join(self.data_path,self.sample_name) - - self.build_filename() + self.get_available_detectors() + self.get_enabled_detectors() + if len(self.enabled_detectors) > 1: + print('WARNING! More than 1 detector enabled') + self.show_enabled_detectors() + elif len(self.enabled_detectors) == 0: + print("WARNING! No detector enabled!!") + self.show_available_detectors() + else: + self.show_enabled_detectors() + self.det = self.enabled_detectors[0] + self.device_name = self.enabled_detectors[0].name + self.build_filename() + self.exposure_time = self.det.cfgExposure.get() + self.exposure_period = self.det.cfgFramerate.get() + self.roix = self.det.cfgRoiX.get() + self.roiy = self.det.cfgRoiY.get() + def build_filename(self, acquisition_type='data'): """ @@ -130,6 +141,99 @@ class Measurement: self.build_filename() + def disable_detector(self, detector_name): + """ + Disable detector + """ + + if detector_name not in self.available_detectors_names: + print("WARNING! The detector " + str(detector_name + " is not available!")) + print("You can check the available detectors with the command dev.show_all()") + + devices_to_be_disabled = [obj for obj in dev.get_devices_with_tags(detector_name)] + for i in range(len(devices_to_be_disabled)): + devices_to_be_disabled[i].enabled = False + + self.get_enabled_detectors() + + if len(self.enabled_detectors) > 1: + print('WARNING! More than 1 detector enabled') + self.show_enabled_detectors() + elif len(self.enabled_detectors) == 0: + print("WARNING! No detector enabled!!") + self.show_available_detectors() + else: + self.show_enabled_detectors() + self.det = self.enabled_detectors[0] + self.device_name = self.enabled_detectors[0].name + self.build_filename() + + + def enable_detector(self, detector_name): + """ + Enable detector + """ + + if detector_name not in self.available_detectors_names: + print("WARNING! The detector " + str(detector_name + " is not available!")) + print("You can check the available detectors with the command dev.show_all()") + + devices_to_be_enabled = [obj for obj in dev.get_devices_with_tags(detector_name)] + for i in range(len(devices_to_be_enabled)): + devices_to_be_enabled[i].enabled = True + dev.daq_stream1.enabled = False + + self.get_enabled_detectors() + + if len(self.enabled_detectors) > 1: + print('WARNING! More than 1 detector enabled') + self.show_enabled_detectors() + elif len(self.enabled_detectors) == 0: + print("WARNING! No detector enabled!!") + self.show_available_detectors() + else: + self.show_enabled_detectors() + self.det = self.enabled_detectors[0] + self.device_name = self.enabled_detectors[0].name + self.build_filename() + + + + def get_available_detectors(self): + """ + Check available detectors + """ + self.available_detectors = [obj for obj in dev.get_devices_with_tags('camera')] + + self.available_detectors_names = [] + for i in range(len(self.available_detectors)): + self.available_detectors_names.append(self.available_detectors[i].name) + + def get_enabled_detectors(self): + """ + Get enabled detectors + """ + self.enabled_detectors = [obj for obj in dev.get_devices_with_tags('camera') if obj.enabled] + + + def show_available_detectors(self): + """ + Show available detectors + """ + print("Available detectors:") + for i in range(len(self.available_detectors)): + print(self.available_detectors[i].name + '\tenabled ' + str(self.available_detectors[i].enabled)) + + + def show_enabled_detectors(self): + """ + List enabled detectors + """ + print("Enabled detectors:") + for i in range(len(self.enabled_detectors)): + print(self.enabled_detectors[i].name) + + def show_all(self): """ @@ -145,18 +249,22 @@ class Measurement: print("Number of flats: " + str(self.nimages_white)) if self.exposure_time == None: print("Exposure time: " + str(self.det.cfgExposure.get())) + self.exposure_time = self.det.cfgExposure.get() else: print("Exposure time: " + str(self.exposure_time)) if self.exposure_period == None: - print("Exposure peirod: " + str(self.det.cfgFramerate.get())) + print("Exposure period: " + str(self.det.cfgFramerate.get())) + self.exposure_period = self.det.cfgFramerate.get() else: print("Exposure period: " + str(self.exposure_period)) if self.roix == None: print("Roix: " + str(self.det.cfgRoiX.get())) + self.roix = self.det.cfgRoiX.get() else: print("Roix: " + str(self.roix)) if self.roiy == None: print("Roiy: " + str(self.det.cfgRoiY.get())) + self.roiy = self.det.cfgRoiY.get() else: print("Roiy: " + str(self.roiy)) print("Start angle: " + str(self.start_angle)) @@ -165,14 +273,14 @@ class Measurement: print("Sample position out: " + str(self.sample_position_out)) - def acquire_darks(self,nimages_dark, exposure_time=None, exposure_period=None, + def acquire_darks(self,nimages_dark=None, exposure_time=None, exposure_period=None, roix=None, roiy=None, acq_mode=None): """ Acquire a set of dark images with shutters closed. Parameters ---------- - nimages_dark : int + nimages_dark : int, optional Number of dark images to acquire (no default) exposure_time : float, optional Exposure time [ms]. If not specified, the currently configured value on the camera will be used @@ -187,18 +295,11 @@ class Measurement: Example: -------- - m.acquire_darks(100, exposure_time=5) + m.acquire_darks(nimages_darks=100, exposure_time=5) """ - # dev.es1_tasks.enabled = False - # dev.es1_psod.enabled = False - # dev.es1_ddaq.enabled = False - # dev.es1_ismc.enabled = False - # dev.es1_roty.enabled = False - dev.gfcam.enabled = True - dev.gfdaq.enabled = True - dev.daq_stream0.enabled = True - dev.daq_stream1.enabled = False - + + if nimages_dark != None: + self.nimages_dark = nimages_dark if exposure_time != None: self.exposure_time = exposure_time if exposure_period != None: @@ -212,11 +313,12 @@ class Measurement: ### TODO: camera reset print("Handing over to 'scans.acquire_dark") - scans.acquire_dark(exp_burst=nimages_dark, exp_time=self.exposure_time, exp_period=self.exposure_period, image_width=self.roix, + scans.acquire_dark(exp_burst=self.nimages_dark, exp_time=self.exposure_time, exp_period=self.exposure_period, image_width=self.roix, image_height=self.roiy, acq_mode=acq_mode, file_path=self.file_path, nr_writers=2, base_path=self.base_path, file_prefix=self.file_prefix) - def acquire_whites(self,nimages_whites, sample_angle_out=None, sample_position_out=None, + + def acquire_whites(self,nimages_white=None, sample_angle_out=None, sample_position_out=None, exposure_time=None, exposure_period=None, roix=None, roiy=None, acq_mode=None): """ @@ -224,7 +326,7 @@ class Measurement: Parameters ---------- - nimages_whites : int + nimages_whites : int, optional Number of white images to acquire (no default) sample_angle_out : float, optional Sample rotation angle for sample out of the beam [deg] @@ -243,18 +345,11 @@ class Measurement: Example: -------- - m.acquire_whites(100, exposure_time=5) + m.acquire_whites(nimages_whites=100, exposure_time=5) """ - # dev.es1_tasks.enabled = False - # dev.es1_psod.enabled = False - # dev.es1_ddaq.enabled = False - # dev.es1_ismc.enabled = False - # dev.es1_roty.enabled = False - dev.gfcam.enabled = True - dev.gfdaq.enabled = True - dev.daq_stream0.enabled = True - dev.daq_stream1.enabled = False - + + if nimages_white != None: + self.nimages_white = nimages_white if sample_angle_out != None: self.sample_angle_out = sample_angle_out if sample_position_out != None: @@ -272,7 +367,78 @@ class Measurement: ### TODO: camera reset print("Handing over to 'scans.acquire_whites") - scans.acquire_white(exp_burst=nimages_whites, sample_angle_out=self.sample_angle_out, sample_position_out= self.sample_position_out, + scans.acquire_white(exp_burst=self.nimages_white, sample_angle_out=self.sample_angle_out, sample_position_out= self.sample_position_out, exp_time=self.exposure_time, exp_period=self.exposure_period, image_width=self.roix, image_height=self.roiy, acq_mode=acq_mode, file_path=self.file_path, nr_writers=2, base_path=self.base_path, - file_prefix=self.file_prefix) \ No newline at end of file + file_prefix=self.file_prefix) + + + def acquire_refs(self,nimages_dark=None, nimages_white=None, sample_angle_out=None, + sample_position_in=None, sample_position_out=None, + exposure_time=None, exposure_period=None, + roix=None, roiy=None, acq_mode=None): + """ + Acquire reference images (darks + whites) and return to beam position. + + Reference images are acquired automatically in an optimized sequence and + the sample is returned to the sample_in_position afterwards. + + Parameters + ---------- + darks : int, optional + Number of dark images to acquire (no default) + nimages_whites : int, optional + Number of white images to acquire (no default) + sample_angle_out : float, optional + Sample rotation angle for sample out of the beam [deg] + sample_position_in : float, optional + Sample stage X position for sample in of the beam [um] + sample_position_out : float, optional + Sample stage X position for sample out of the beam [um] + exposure_time : float, optional + Exposure time [ms]. If not specified, the currently configured value on the camera will be used + exposure_period : float, optional + Exposure period [ms] + roix : int, optional + ROI size in the x-direction [pixels] + roiy : int, optional + ROI size in the y-direction [pixels] + acq_mode : str, optional + Predefined acquisition mode (default=None) + + Example: + -------- + m.acquire_refs(nimages_whites=100, exposure_time=5) + """ + + if nimages_dark != None: + self.nimages_dark = nimages_dark + if nimages_white != None: + self.nimages_white = nimages_white + if sample_angle_out != None: + self.sample_angle_out = sample_angle_out + if sample_position_in != None: + self.sample_position_in = sample_position_in + if sample_position_out != None: + self.sample_position_out = sample_position_out + if exposure_time != None: + self.exposure_time = exposure_time + if exposure_period != None: + self.exposure_period = exposure_period + if roix != None: + self.roix = roix + if roiy != None: + self.roiy = roiy + + self.build_filename(acquisition_type='dark') + file_prefix_dark = self.file_prefix + self.build_filename(acquisition_type='white') + file_prefix_white = self.file_prefix + + ### TODO: camera reset + print("Handing over to 'scans.acquire_refs") + scans.acquire_refs(num_darks=self.nimages_dark, num_flats=self.nimages_white, sample_angle_out=self.sample_angle_out, + sample_position_in=self.sample_position_in, sample_position_out=self.sample_position_out, + exp_time=self.exposure_time, exp_period=self.exposure_period, image_width=self.roix, + image_height=self.roiy, acq_mode='default', file_path=self.file_path, nr_writers=2, base_path=self.base_path, + file_prefix_dark=file_prefix_dark, file_prefix_white=file_prefix_white) \ No newline at end of file -- 2.49.1 From aea898e60f451f01ac626064e61764f236958d00 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Tue, 11 Feb 2025 17:53:46 +0100 Subject: [PATCH 12/14] BEC raises on warnings --- tomcat_bec/device_configs/microxas_test_bed.yaml | 7 +++++++ tomcat_bec/devices/gigafrost/stddaq_client.py | 2 +- tomcat_bec/scans/tutorial_fly_scan.py | 16 ++++++++-------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 8a98eee..dc99dc0 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -129,6 +129,7 @@ gfcam: deviceTags: - camera - trigger + - gfcam enabled: true onFailure: buffer readOnly: false @@ -144,6 +145,7 @@ gfdaq: data_source_name: 'gfcam' deviceTags: - std-daq + - gfcam enabled: true onFailure: buffer readOnly: false @@ -157,6 +159,7 @@ daq_stream0: url: 'tcp://129.129.95.111:20000' deviceTags: - std-daq + - gfcam enabled: true onFailure: buffer readOnly: false @@ -170,6 +173,7 @@ daq_stream1: url: 'tcp://129.129.95.111:20001' deviceTags: - std-daq + - gfcam enabled: true onFailure: buffer readOnly: false @@ -184,6 +188,7 @@ pcocam: prefix: 'X02DA-CCDCAM2:' deviceTags: - camera + - pcocam enabled: true onFailure: buffer readOnly: false @@ -198,6 +203,7 @@ pcodaq: rest_url: 'http://129.129.95.111:5010' deviceTags: - std-daq + - pcocam enabled: true onFailure: buffer readOnly: false @@ -211,6 +217,7 @@ pco_stream0: url: 'tcp://129.129.95.111:20010' deviceTags: - std-daq + - pcocam enabled: true onFailure: buffer readOnly: false diff --git a/tomcat_bec/devices/gigafrost/stddaq_client.py b/tomcat_bec/devices/gigafrost/stddaq_client.py index e9d51d9..3ad65aa 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_client.py +++ b/tomcat_bec/devices/gigafrost/stddaq_client.py @@ -285,7 +285,7 @@ class StdDaqClient(PSIDeviceBase): # Stop if current status is not idle if self.state() != "idle": - raise RuntimeWarning(f"[{self.name}] stdDAQ reconfiguration might corrupt files") + logger.warning(f"[{self.name}] stdDAQ reconfiguration might corrupt files") # Update retrieved config cfg['image_pixel_height'] = int(self.cfg_pixel_height.get()) diff --git a/tomcat_bec/scans/tutorial_fly_scan.py b/tomcat_bec/scans/tutorial_fly_scan.py index fe0ea53..0aba4e8 100644 --- a/tomcat_bec/scans/tutorial_fly_scan.py +++ b/tomcat_bec/scans/tutorial_fly_scan.py @@ -247,14 +247,14 @@ class AcquireRefs(Acquire): self.num_flats = num_flats self.file_prefix_dark = file_prefix_dark self.file_prefix_white = file_prefix_white - self.exp_time = exp_time, - self.exp_period = exp_period, - self.image_width = image_width, - self.image_height = image_height, - self.acq_mode = acq_mode, - self.file_path = file_path, - self.nr_writers = nr_writers, - self.base_path = base_path, + self.exp_time = exp_time + self.exp_period = exp_period + self.image_width = image_width + self.image_height = image_height + self.acq_mode = acq_mode + self.file_path = file_path + self.nr_writers = nr_writers + self.base_path = base_path def scan_core(self): -- 2.49.1 From 163ef3c7a5e9c0195ee37d82b774ac3236e392c8 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Thu, 13 Feb 2025 18:16:42 +0100 Subject: [PATCH 13/14] PCO step scanning works --- .../device_configs/microxas_test_bed.yaml | 3 +- tomcat_bec/devices/gigafrost/pcoedgecamera.py | 348 +++++++++++------- tomcat_bec/devices/gigafrost/stddaq_client.py | 2 +- tomcat_bec/scans/tomcat_scans.py | 2 +- tomcat_bec/scans/tutorial_fly_scan.py | 2 +- tomcat_bec/scripts/anotherroundsans.py | 168 +++++++-- tomcat_bec/scripts/demoscans.py | 4 +- 7 files changed, 358 insertions(+), 171 deletions(-) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index dc99dc0..09c760e 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -188,12 +188,13 @@ pcocam: prefix: 'X02DA-CCDCAM2:' deviceTags: - camera + - trigger - pcocam enabled: true onFailure: buffer readOnly: false readoutPriority: monitored - softwareTrigger: false + softwareTrigger: true pcodaq: description: GigaFrost stdDAQ client diff --git a/tomcat_bec/devices/gigafrost/pcoedgecamera.py b/tomcat_bec/devices/gigafrost/pcoedgecamera.py index ed095eb..32899e0 100644 --- a/tomcat_bec/devices/gigafrost/pcoedgecamera.py +++ b/tomcat_bec/devices/gigafrost/pcoedgecamera.py @@ -6,16 +6,20 @@ Created on Wed Dec 6 11:33:54 2023 """ from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind -from ophyd.status import SubscriptionStatus +from ophyd.status import SubscriptionStatus, DeviceStatus 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 +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") @@ -24,42 +28,43 @@ class PcoEdgeCameraMixin(CustomDeviceMixin): This class will be called by the custom_prepare_cls attribute of the detector class. """ + def on_stage(self) -> None: - """Configure and arm PCO.Edge camera for acquisition - """ + """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!") + 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 'num_images_total' in scanargs and scanargs['num_images_total'] is not None: - d['images_total'] = scanargs['num_images_total'] - 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 "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'] - + 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: @@ -70,29 +75,86 @@ class PcoEdgeCameraMixin(CustomDeviceMixin): self.parent.bluestage() def on_unstage(self) -> None: - """Disarm the PCO.Edge camera - """ + """Disarm the PCO.Edge camera""" self.parent.blueunstage() def on_stop(self) -> None: - """Stop the PCO.Edge camera - """ + """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. + """ + logger.warning(f"triggering PCO") + # 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 didWeReset(*args, old_value, value, timestamp, **kwargs): + return (value < old_value) or (value == 0) + + self.parent.buffer_clear.set(1).wait() + status = SubscriptionStatus(self.parent.buffer_used, didWeReset, 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 areWeDoneYet(*args, old_value, value, timestamp, **kwargs): + num_target = self.parent.file_savestop.get() + # logger.warning(f"{value} of {num_target}") + return bool(value >= num_target) + + status = SubscriptionStatus( + self.parent.buffer_used, areWeDoneYet, timeout=max(5, 5 * t_expected), 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 haveWeSentIt(*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, haveWeSentIt, timeout=120, settle_time=0.2 + ) + status.wait() + logger.warning(f"done PCO") + class HelgeCameraBase(PSIDeviceBase): """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 + 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). + 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 @@ -112,12 +174,12 @@ class HelgeCameraBase(PSIDeviceBase): 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 = Component(EpicsSignalRO, "QUERY", kind=Kind.config, doc="Camera model info") - model = Component(EpicsSignalRO, "BOARD", kind=Kind.omitted, doc="Camera board info") + model = Component(EpicsSignalRO, "BOARD", kind=Kind.omitted, doc="Camera board info") # ######################################################################## # Acquisition commands @@ -125,30 +187,41 @@ class HelgeCameraBase(PSIDeviceBase): # ######################################################################## # 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) + 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") + 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) + camWarning = Component(EpicsSignalRO, "WARNCODE", auto_monitor=True, kind=Kind.config) # ######################################################################## - # Buffer configuration + # 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 @@ -158,15 +231,18 @@ class HelgeCameraBase(PSIDeviceBase): 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) + 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) + camInit = Component(EpicsSignalRO, "INIT", auto_monitor=True, kind=Kind.config) camInitBusy = Component(EpicsSignalRO, "BUSY_INIT", auto_monitor=True, kind=Kind.config) # ######################################################################## @@ -175,22 +251,24 @@ class HelgeCameraBase(PSIDeviceBase): # ######################################################################## # 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) + # 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""" + """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 "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: + # if self.camRemoval.value==0 and self.camInit.value==0: + if self.camInit.value == 0: return "OFFLINE" - #if self.camRemoval.value: + # if self.camRemoval.value: # return "REMOVED" return "UNKNOWN" @@ -199,103 +277,113 @@ class HelgeCameraBase(PSIDeviceBase): raise ReadOnlyError("State is a ReadOnly property") def configure(self, d: dict = {}) -> tuple: - """ Configure the base Helge camera device - - Parameters as 'd' dictionary - ---------------------------- - num_images : int - Number of images to be taken during each scan. Meaning depends on - store mode. - exposure_time_ms : float - Exposure time [ms], usually gets set back to 20 ms - exposure_period_ms : float - Exposure period [ms], up to 200 ms. - store_mode : str - Buffer operation mode - *'Recorder' to record in buffer - *'FIFO buffer' for continous streaming - data_format : str - Usually set to 'ZEROMQ' + """Configure the base Helge camera device + + Parameters as 'd' dictionary + ---------------------------- + num_images : int + Number of images to be taken during each scan. Meaning depends on + store mode. + exposure_time_ms : float + Exposure time [ms], usually gets set back to 20 ms + exposure_period_ms : float + Exposure period [ms], up to 200 ms. + store_mode : str + Buffer operation mode + *'Recorder' to record in buffer + *'FIFO buffer' for continous streaming + data_format : str + Usually set to 'ZEROMQ' """ if self.state not in ("IDLE"): raise RuntimeError(f"Can't change configuration from state {self.state}") - + # If Bluesky style configure if d is not None: # Commonly changed settings - if 'num_images' in d: - self.file_savestop.set(d['num_images']).wait() - if 'exposure_time_ms' in d: - self.acquire_time.set(d['exposure_time_ms']).wait() - if 'exposure_period_ms' in d: - 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 'store_mode' in d: - self.bufferStoreMode.set(d['store_mode']).wait() - if 'data_format' in d: - self.file_format.set(d['data_format']).wait() + if "exposure_num_burst" in d: + self.file_savestop.set(d["exposure_num_burst"]).wait() + if "exposure_time_ms" in d: + self.acquire_time.set(d["exposure_time_ms"]).wait() + if "exposure_period_ms" in d: + 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 "store_mode" in d: + self.bufferStoreMode.set(d["store_mode"]).wait() + if "data_format" in d: + self.file_format.set(d["data_format"]).wait() # State machine # Initial: BUSY and SET both low - # 0. Write 1 to SET_PARAM - # 1. BUSY goes high, SET stays 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 bluestage(self): - """Bluesky style stage: arm the detector - """ + """Bluesky style stage: arm the detector""" + logger.warning("Staging PCO") # 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}") + if ( + self.bufferStoreMode.get() in ("Recorder", 0) + and self.file_savestop.get() > self.buffer_size.get() + ): + self.logger.warning( + "You're about to send some empty images, {self.file_savestop.get()} is above buffer size" + ) + # 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(value==6) + return bool(value == 6) + status = SubscriptionStatus(self.camStatusCode, isRunning, timeout=5, settle_time=0.2) status.wait() def blueunstage(self): - """Bluesky style unstage: stop the detector - """ + """Bluesky style unstage: stop the detector""" self.camStatusCmd.set("Idle").wait() self.custom_prepare.stop_monitor = True - # Subscribe and wait for update - def isIdle(*args, old_value, value, timestamp, **kwargs): - return bool(value==2) - status = SubscriptionStatus(self.camStatusCode, isIdle, timeout=5, settle_time=0.2) - status.wait() - # Data streaming is stopped by setting the max index to 0 + # FIXME: This might interrupt data transfer self.file_savestop.set(0).wait() - def bluekickoff(self): - """ Start data transfer - + """Start data transfer + TODO: Need to revisit this once triggering is complete """ self.file_transfer.set(1).wait() + # def complete(self): + # """ Wait until the images have been sent""" + # def areWeSending(*args, value, timestamp, **kwargs): + # return not bool(value) + # status = SubscriptionStatus(self.file_savebusy, haveWeSentIt, timeout=None, settle_time=0.2) + # return status 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 + + 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. """ @@ -310,28 +398,36 @@ class PcoEdge5M(HelgeCameraBase): camRate = Component(EpicsSignalRO, "CAMRATE", auto_monitor=True, kind=Kind.config) # ######################################################################## - # Acquisition configuration + # 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) - + # 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) + 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 + 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: @@ -360,18 +456,18 @@ class PcoEdge5M(HelgeCameraBase): # 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.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() + 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) @@ -379,7 +475,7 @@ class PcoEdge5M(HelgeCameraBase): # Automatically connect to test camera if directly invoked if __name__ == "__main__": - + # Drive data collection cam = PcoEdgeBase("X02DA-CCDCAM2:", name="mcpcam") cam.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/stddaq_client.py b/tomcat_bec/devices/gigafrost/stddaq_client.py index 3ad65aa..fc4603f 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_client.py +++ b/tomcat_bec/devices/gigafrost/stddaq_client.py @@ -119,7 +119,7 @@ class StdDaqMixin(CustomDeviceMixin): for msg in self.parent._wsclient: message = json.loads(msg) self.parent.runstatus.put(message["status"], force=True) - logger.info(f"[{self.parent.name}] Pushed status: {message['status']}") + # logger.info(f"[{self.parent.name}] Pushed status: {message['status']}") except (ConnectionClosedError, ConnectionClosedOK, AssertionError): # Libraty throws theese after connection is closed return diff --git a/tomcat_bec/scans/tomcat_scans.py b/tomcat_bec/scans/tomcat_scans.py index 5b59516..c6e980d 100644 --- a/tomcat_bec/scans/tomcat_scans.py +++ b/tomcat_bec/scans/tomcat_scans.py @@ -97,7 +97,7 @@ class TomcatStepScan(ScanBase): yield from self._move_scan_motors_and_wait(pos) time.sleep(self.settling_time) - trigger_time = self.exp_time * self.burst_at_each_point + trigger_time = 0.001*self.exp_time * self.burst_at_each_point yield from self.stubs.trigger(min_wait=trigger_time) # yield from self.stubs.trigger(group='trigger', point_id=self.point_id) # time.sleep(trigger_time) diff --git a/tomcat_bec/scans/tutorial_fly_scan.py b/tomcat_bec/scans/tutorial_fly_scan.py index 0aba4e8..0f3586e 100644 --- a/tomcat_bec/scans/tutorial_fly_scan.py +++ b/tomcat_bec/scans/tutorial_fly_scan.py @@ -46,7 +46,7 @@ class AcquireDark(Acquire): def scan_core(self): # close the shutter -# yield from self._move_scan_motors_and_wait(self.dark_shutter_pos) + yield from self._move_scan_motors_and_wait(self.dark_shutter_pos) #yield from self.stubs.set_and_wait(device=[self.shutter], positions=[0]) yield from super().scan_core() diff --git a/tomcat_bec/scripts/anotherroundsans.py b/tomcat_bec/scripts/anotherroundsans.py index d8d9247..17c5388 100644 --- a/tomcat_bec/scripts/anotherroundsans.py +++ b/tomcat_bec/scripts/anotherroundsans.py @@ -18,6 +18,25 @@ def dev_disable_all(): dev[k].enabled = False +def cam_select(camera: str): + """Select the active camera pipeline""" + if isinstance(camera, str): + if camera in ("gf", "gf2", "gigafrost"): + dev.gfcam.enabled = True + dev.gfdaq.enabled = True + dev.daq_stream0.enabled = True + dev.pcocam.enabled = False + dev.pcodaq.enabled = False + dev.pco_stream0.enabled = False + if camera in ("pco", "pco.edge", "pcoedge"): + dev.gfcam.enabled = False + dev.gfdaq.enabled = False + dev.daq_stream0.enabled = False + dev.pcocam.enabled = True + dev.pcodaq.enabled = True + dev.pco_stream0.enabled = True + + def anotherstepscan( scan_start, @@ -26,8 +45,9 @@ def anotherstepscan( exp_time=0.005, exp_burst=5, settling_time=0, - image_width=2016, - image_height=2016, + camera=None, + image_width=None, + image_height=None, sync="inp1", **kwargs ): @@ -41,31 +61,49 @@ def anotherstepscan( -------- demostepscan(scan_start=-32, scan_end=148, steps=180, exp_time=0.005, exp_burst=5) """ - # if not bl_check_beam(): - # raise RuntimeError("Beamline is not in ready state") + # Check beamline status before scan + if not bl_check_beam(): + raise RuntimeError("Beamline is not in ready state") + # Enable the correct camera before scan + cam_select(camera) + # This scan uses software triggering + if isinstance(camera, str): + if camera in ("gf", "gf2", "gigafrost"): + dev.gfcam.software_trigger = True + if camera in ("pco", "pco.edge", "pcoedge"): + dev.pcocam.software_trigger = True - dev_disable_all() + # Disable aerotech controller + # dev.es1_tasks.enabled = False + # dev.es1_psod.enabled = False + # dev.es1_ddaq.enabled = True + # Enable scan motor dev.es1_roty.enabled = True - #dev.es1_ddaq.enabled = True - dev.gfcam.enabled = True - dev.gfdaq.enabled = True - dev.daq_stream0.enabled = True - dev.daq_stream1.enabled = False + + print("Handing over to 'scans.tomcatstepscan'") - scans.tomcatstepscan( - scan_start=scan_start, - scan_end=scan_end, - steps=steps, - exp_time=exp_time, - exp_burst=exp_burst, - relative=False, - image_width=image_width, - image_height=image_height, - settling_time=settling_time, - sync=sync, - **kwargs - ) + try: + scans.tomcatstepscan( + scan_start=scan_start, + scan_end=scan_end, + steps=steps, + exp_time=exp_time, + exp_burst=exp_burst, + relative=False, + image_width=image_width, + image_height=image_height, + settling_time=settling_time, + sync=sync, + **kwargs + ) + finally: + # if isinstance(camera, str): + # if camera in ("gf", "gf2", "gigafrost"): + # dev.gfcam.software_trigger = False + # if camera in ("pco", "pco.edge", "pcoedge"): + # dev.pcocam.software_trigger = False + pass @@ -78,8 +116,9 @@ def anothersequencescan( repmode="PosNeg", exp_time=0.005, exp_burst=180, - image_width=2016, - image_height=2016, + camera=None, + image_width=None, + image_height=None, sync="pso", **kwargs ): @@ -96,16 +135,15 @@ def anothersequencescan( -------- >>> anothersequencescan(33, 180, 180, exp_time=0.005, exp_frames=1800, repeats=10) """ + # Check beamline status before scan if not bl_check_beam(): raise RuntimeError("Beamline is not in ready state") - + # Enable the correct camera before scan + cam_select(camera) + # Enabling aerotech controller dev.es1_tasks.enabled = True dev.es1_psod.enabled = False dev.es1_ddaq.enabled = True - dev.gfcam.enabled = True - dev.gfdaq.enabled = True - dev.daq_stream0.enabled = True - dev.daq_stream1.enabled = False print("Handing over to 'scans.sequencescan'") scans.tomcatsimplesequencescan( @@ -131,8 +169,9 @@ def anothersnapnstepscan( steps, exp_time=0.005, exp_burst=180, - image_width=2016, - image_height=2016, + camera=None, + image_width=None, + image_height=None, settling_time=0.1, sync="pso", **kwargs @@ -148,17 +187,15 @@ def anothersnapnstepscan( -------- >>> anothersnapnstepscan(33, 180, 180, exp_time=0.005, exp_frames=1800, repeats=10) """ + # Check beamline status before scan if not bl_check_beam(): raise RuntimeError("Beamline is not in ready state") - + # Enable the correct camera before scan + cam_select(camera) + # Enabling aerotech controller dev.es1_tasks.enabled = True dev.es1_psod.enabled = False dev.es1_ddaq.enabled = True - dev.gfcam.enabled = True - dev.gfdaq.enabled = True - dev.daq_stream0.enabled = True - dev.daq_stream1.enabled = False - print("Handing over to 'scans.tomcatsnapnstepscan'") scans.tomcatsnapnstepscan( @@ -174,3 +211,58 @@ def anothersnapnstepscan( **kwargs ) + + + + + + +def ascan(motor, scan_start, scan_end, steps, exp_time, datasource, visual=True, **kwargs): + """Demo step scan with plotting + + This is a simple user-space demo step scan with automatic plorring and fitting via the BEC. + It's mostly a standard BEC scan, just adds GUI setup and fits a hardcoded LlinearModel at + the end (other models are more picky on the initial parameters). + + Example: + -------- + ascan(dev.dccm_energy, 12,13, steps=21, exp_time=0.1, datasource=dev.dccm_xbpm) + """ + # Dummy method to check beamline status + if not bl_check_beam(): + raise RuntimeError("Beamline is not in ready state") + + if visual: + # Get or create scan specific window + window = None + for _, val in bec.gui.windows.items(): + if val.title == "CurrentScan": + window = val.widget + window.clear_all() + if window is None: + window = bec.gui.new("CurrentScan") + + # Draw a simploe plot in the window + dock = window.add_dock(f"ScanDisplay {motor}") + plt1 = dock.add_widget("BECWaveformWidget") + plt1.plot(x_name=motor, y_name=datasource) + plt1.set_x_label(motor) + plt1.set_y_label(datasource) + plt1.add_dap(motor, datasource, dap="LinearModel") + window.show() + + print("Handing over to 'scans.line_scan'") + if "relative" in kwargs: + del kwargs["relative"] + s = scans.line_scan( + motor, scan_start, scan_end, steps=steps, exp_time=exp_time, relative=False, **kwargs + ) + + if visual: + fit = plt1.get_dap_params() + else: + fit = bec.dap.LinearModel.fit(s, motor.name, motor.name, datasource.name, datasource.name) + + # TODO: Move to fitted value... like center, peak, edge, etc... + + return s, fit \ No newline at end of file diff --git a/tomcat_bec/scripts/demoscans.py b/tomcat_bec/scripts/demoscans.py index 8e7ee8d..c0e55c1 100644 --- a/tomcat_bec/scripts/demoscans.py +++ b/tomcat_bec/scripts/demoscans.py @@ -4,9 +4,7 @@ def bl_check_beam(): """Checks beamline status""" - motor_enabled = bool(dev.es1_roty.motor_enable.get()) - result = motor_enabled - return result + return True def demostepscan( -- 2.49.1 From b8dcda16961a36ce9cf99d65cae3e8e5fa2eaaad Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Fri, 14 Feb 2025 13:34:02 +0100 Subject: [PATCH 14/14] Linting to 9.22 --- .../devices/gigafrost/gigafrostcamera.py | 174 ++++++------ tomcat_bec/devices/gigafrost/pco_datasink.py | 55 ++-- tomcat_bec/devices/gigafrost/pcoedgecamera.py | 78 +++--- tomcat_bec/devices/gigafrost/stddaq_client.py | 252 ++++++++++-------- 4 files changed, 297 insertions(+), 262 deletions(-) diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index 186badf..59ee53f 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -26,9 +26,11 @@ except ModuleNotFoundError: try: from bec_lib import bec_logger + logger = bec_logger.logger except ModuleNotFoundError: import logging + logger = logging.getLogger("GfCam") @@ -38,8 +40,9 @@ class GigaFrostCameraMixin(CustomDetectorMixin): This class will be called by the custom_prepare_cls attribute of the detector class. """ + # pylint: disable=protected-access def _define_backend_ip(self): - """ Select backend IP address for UDP stream""" + """Select backend IP address for UDP stream""" if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_IP, const.BE3_SOUTH_IP if self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: @@ -48,7 +51,7 @@ class GigaFrostCameraMixin(CustomDetectorMixin): raise RuntimeError(f"Backend {self.parent.backendUrl.get()} not recognized.") def _define_backend_mac(self): - """ Select backend MAC address for UDP stream""" + """Select backend MAC address for UDP stream""" if self.parent.backendUrl.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33 return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC if self.parent.backendUrl.get() == const.BE999_DAFL_CLIENT: @@ -74,7 +77,7 @@ class GigaFrostCameraMixin(CustomDetectorMixin): self.parent.macSouth.get(), self.parent.ipSouth.get(), dest_port, - source_port + source_port, ) else: extend_header_table( @@ -82,20 +85,20 @@ class GigaFrostCameraMixin(CustomDetectorMixin): self.parent.macNorth.get(), self.parent.ipNorth.get(), dest_port, - source_port + source_port, ) return udp_header_table def on_init(self) -> None: - """ Initialize the camera, set channel values""" + """Initialize the camera, set channel values""" # ToDo: Not sure if it's a good idea to change camera settings upon # ophyd device startup, i.e. each deviceserver restart. self._init_gigafrost() self.parent._initialized = True def _init_gigafrost(self) -> None: - """ Initialize the camera, set channel values""" + """Initialize the camera, set channel values""" # Stop acquisition self.parent.cmdStartCamera.set(0).wait() @@ -142,7 +145,7 @@ class GigaFrostCameraMixin(CustomDetectorMixin): return super().on_init() def on_stage(self) -> None: - """ Configuration and staging + """Configuration and staging In the BEC model ophyd devices must fish out their own configuration from the 'scaninfo'. I.e. they need to know which parameters are relevant for them at each scan. @@ -158,25 +161,23 @@ class GigaFrostCameraMixin(CustomDetectorMixin): logger.warning( f"[{self.parent.name}] Ophyd device havent ran the initialization sequence," "IOC might be in unknown configuration." - ) + ) # 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_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'] + if "kwargs" in scanparam: + scanargs = scanparam["kwargs"] + 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_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" @@ -217,13 +218,17 @@ class GigaFrostCameraMixin(CustomDetectorMixin): Specify actions to be executed upon receiving trigger signal. Return a DeviceStatus object or None """ - if self.parent.infoBusyFlag.get() in (0, 'IDLE'): - raise RuntimeError('GigaFrost must be running before triggering') + if self.parent.infoBusyFlag.get() in (0, "IDLE"): + raise RuntimeError("GigaFrost must be running before triggering") logger.warning(f"[{self.parent.name}] SW triggering gigafrost") # Soft triggering based on operation mode - if self.parent.autoSoftEnable.get() and self.parent.trigger_mode == 'auto' and self.parent.enable_mode == 'soft': + if ( + self.parent.autoSoftEnable.get() + and self.parent.trigger_mode == "auto" + and self.parent.enable_mode == "soft" + ): # BEC teststand operation mode: posedge of SoftEnable if Started self.parent.cmdSoftEnable.set(0).wait() self.parent.cmdSoftEnable.set(1).wait() @@ -254,6 +259,7 @@ class GigaFrostCamera(PSIDetectorBase): ---------- FRAMERATE : Ignored in soft trigger mode, period becomes 2xExposure time """ + # pylint: disable=too-many-instance-attributes custom_prepare_cls = GigaFrostCameraMixin @@ -267,12 +273,13 @@ class GigaFrostCamera(PSIDetectorBase): cmdSetParam = Component(EpicsSignal, "SET_PARAM.PROC", put_complete=True, kind=Kind.omitted) cfgAcqMode = Component(EpicsSignal, "ACQMODE", put_complete=True, kind=Kind.config) - array_size = DynamicDeviceComponent({ - "array_size_x": (EpicsSignalRO, "ROIX", {'auto_monitor': True}), - "array_size_y": (EpicsSignalRO, "ROIY", {'auto_monitor': True}), - }, doc="Size of the array in the XY dimensions") - - + array_size = DynamicDeviceComponent( + { + "array_size_x": (EpicsSignalRO, "ROIX", {"auto_monitor": True}), + "array_size_y": (EpicsSignalRO, "ROIY", {"auto_monitor": True}), + }, + doc="Size of the array in the XY dimensions", + ) # UDP header cfgUdpNumPorts = Component(EpicsSignal, "PORTS", put_complete=True, kind=Kind.config) @@ -282,24 +289,26 @@ class GigaFrostCamera(PSIDetectorBase): # Standard camera configs cfgExposure = Component( - EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config) + EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config + ) cfgFramerate = Component( - EpicsSignal, "FRAMERATE", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgRoiX = Component( - EpicsSignal, "ROIX", put_complete=True, auto_monitor=True, kind=Kind.config) - cfgRoiY = Component( - EpicsSignal, "ROIY", put_complete=True, auto_monitor=True, kind=Kind.config) + EpicsSignal, "FRAMERATE", put_complete=True, auto_monitor=True, kind=Kind.config + ) + cfgRoiX = Component(EpicsSignal, "ROIX", put_complete=True, auto_monitor=True, kind=Kind.config) + cfgRoiY = Component(EpicsSignal, "ROIY", put_complete=True, auto_monitor=True, kind=Kind.config) cfgScanId = Component( - EpicsSignal, "SCAN_ID", put_complete=True, auto_monitor=True, kind=Kind.config) + EpicsSignal, "SCAN_ID", put_complete=True, auto_monitor=True, kind=Kind.config + ) cfgCntNum = Component( - EpicsSignal, "CNT_NUM", put_complete=True, auto_monitor=True, kind=Kind.config) + EpicsSignal, "CNT_NUM", put_complete=True, auto_monitor=True, kind=Kind.config + ) cfgCorrMode = Component( - EpicsSignal, "CORR_MODE", put_complete=True, auto_monitor=True, kind=Kind.config) + EpicsSignal, "CORR_MODE", put_complete=True, auto_monitor=True, kind=Kind.config + ) # Software signals cmdSoftEnable = Component(EpicsSignal, "SOFT_ENABLE", put_complete=True) - cmdSoftTrigger = Component( - EpicsSignal, "SOFT_TRIG.PROC", put_complete=True, kind=Kind.omitted) + cmdSoftTrigger = Component(EpicsSignal, "SOFT_TRIG.PROC", put_complete=True, kind=Kind.omitted) cmdSoftExposure = Component(EpicsSignal, "SOFT_EXP", put_complete=True) cfgAcqMode = Component(EpicsSignal, "ACQMODE", put_complete=True, kind=Kind.config) @@ -403,11 +412,7 @@ class GigaFrostCamera(PSIDetectorBase): kind=Kind.config, ) cfgCntEndBit = Component( - EpicsSignal, - "CNT_ENDBIT_RBV", - write_pv="CNT_ENDBIT", - put_complete=True, - kind=Kind.config + EpicsSignal, "CNT_ENDBIT_RBV", write_pv="CNT_ENDBIT", put_complete=True, kind=Kind.config ) # Line swap selection @@ -457,32 +462,41 @@ class GigaFrostCamera(PSIDetectorBase): ): # Ugly hack to pass values before on_init() self._signals_to_be_set = {} - self._signals_to_be_set['auto_soft_enable'] = auto_soft_enable - self._signals_to_be_set['backend_url'] = backend_url + self._signals_to_be_set["auto_soft_enable"] = auto_soft_enable + self._signals_to_be_set["backend_url"] = backend_url # super() will call the mixin class - super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, device_manager=device_manager, **kwargs) + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + device_manager=device_manager, + **kwargs, + ) def _init(self): """Ugly hack: values must be set before on_init() is called""" # Additional parameters self.autoSoftEnable._metadata["write_access"] = False self.backendUrl._metadata["write_access"] = False - self.autoSoftEnable.put(self._signals_to_be_set['auto_soft_enable'], force=True) - self.backendUrl.put(self._signals_to_be_set['backend_url'], force=True) + self.autoSoftEnable.put(self._signals_to_be_set["auto_soft_enable"], force=True) + self.backendUrl.put(self._signals_to_be_set["backend_url"], force=True) return super()._init() def initialize(self): - """ Initialization in separate command""" + """Initialization in separate command""" self.custom_prepare._init_gigafrost() self._initialized = True def trigger(self) -> DeviceStatus: - """ Sends a software trigger to GigaFrost""" + """Sends a software trigger to GigaFrost""" super().trigger() # There's no status readback from the camera, so we just wait - sleep_time = self.cfgExposure.value*self.cfgCntNum.value*0.001+0.2 + sleep_time = self.cfgExposure.value * self.cfgCntNum.value * 0.001 + 0.2 sleep(sleep_time) return DeviceStatus(self, done=True, success=True, settle_time=sleep_time) @@ -527,40 +541,40 @@ class GigaFrostCamera(PSIDetectorBase): # If Bluesky style configure if d is not None: # Commonly changed settings - if 'exposure_num_burst' in d: - self.cfgCntNum.set(d['exposure_num_burst']).wait() - if 'exposure_time_ms' in d: - self.cfgExposure.set(d['exposure_time_ms']).wait() - if 'exposure_period_ms' in d: - self.cfgFramerate.set(d['exposure_period_ms']).wait() - if 'image_width' in d: - if d['image_width']%48 !=0: + if "exposure_num_burst" in d: + self.cfgCntNum.set(d["exposure_num_burst"]).wait() + if "exposure_time_ms" in d: + self.cfgExposure.set(d["exposure_time_ms"]).wait() + if "exposure_period_ms" in d: + self.cfgFramerate.set(d["exposure_period_ms"]).wait() + if "image_width" in d: + if d["image_width"] % 48 != 0: raise RuntimeError(f"[{self.name}] image_width must be divisible by 48") - self.cfgRoiX.set(d['image_width']).wait() - if 'image_height' in d: - if d['image_height']%16 !=0: + self.cfgRoiX.set(d["image_width"]).wait() + if "image_height" in d: + if d["image_height"] % 16 != 0: raise RuntimeError(f"[{self.name}] image_height must be divisible by 16") - self.cfgRoiY.set(d['image_height']).wait() + self.cfgRoiY.set(d["image_height"]).wait() # Dont change these - scanid = d.get('scanid', 0) - correction_mode = d.get('correction_mode', 5) + scanid = d.get("scanid", 0) + correction_mode = d.get("correction_mode", 5) self.cfgScanId.set(scanid).wait() self.cfgCorrMode.set(correction_mode).wait() - if 'acq_mode' in d: - self.set_acquisition_mode(d['acq_mode']) + if "acq_mode" in d: + self.set_acquisition_mode(d["acq_mode"]) # Commit parameters self.cmdSetParam.set(1).wait() def bluestage(self): - """ Bluesky style stage""" + """Bluesky style stage""" # Switch to acquiring self.cmdStartCamera.set(1).wait() def set_acquisition_mode(self, acq_mode): - """ Set acquisition mode - + """Set acquisition mode + Utility function to quickly select between pre-configured and tested acquisition modes. @@ -577,7 +591,7 @@ class GigaFrostCamera(PSIDetectorBase): self.cfgEnableScheme.set(0).wait() # Set modes - #self.cmdSoftEnable.set(0).wait() + # self.cmdSoftEnable.set(0).wait() self.enable_mode = "soft" self.trigger_mode = "auto" self.exposure_mode = "timer" @@ -834,11 +848,11 @@ class GigaFrostCamera(PSIDetectorBase): The GigaFRoST enable mode. Valid arguments are: * 'soft': - The GigaFRoST enable signal is supplied through a software + The GigaFRoST enable signal is supplied through a software signal * 'external': - The GigaFRoST enable signal is supplied through an external TTL - gating signal from the rotaiton stage or some other control + The GigaFRoST enable signal is supplied through an external TTL + gating signal from the rotaiton stage or some other control unit * 'soft+ext': The GigaFRoST enable signal can be supplied either via the @@ -851,9 +865,7 @@ class GigaFrostCamera(PSIDetectorBase): """ if mode not in const.gf_valid_enable_modes: - raise ValueError( - "Invalid enable mode! Valid modes are:\n{const.gf_valid_enable_modes}" - ) + raise ValueError("Invalid enable mode! Valid modes are:\n{const.gf_valid_enable_modes}") if mode == "soft": self.cfgEnableExt.set(0).wait() diff --git a/tomcat_bec/devices/gigafrost/pco_datasink.py b/tomcat_bec/devices/gigafrost/pco_datasink.py index b76defb..e64f3ac 100644 --- a/tomcat_bec/devices/gigafrost/pco_datasink.py +++ b/tomcat_bec/devices/gigafrost/pco_datasink.py @@ -6,22 +6,19 @@ Created on Thu Jun 27 17:28:43 2024 @author: mohacsi_i """ -import json -import enum from time import sleep, time from threading import Thread import zmq -import numpy as np -from ophyd import Device, Signal, Component, Kind, DeviceStatus +from ophyd import Device, Signal, Component, Kind from ophyd_devices.interfaces.base_classes.psi_detector_base import ( CustomDetectorMixin, PSIDetectorBase, ) from bec_lib import bec_logger -logger = bec_logger.logger -ZMQ_TOPIC_FILTER = b'' +logger = bec_logger.logger +ZMQ_TOPIC_FILTER = b"" class PcoTestConsumerMixin(CustomDetectorMixin): @@ -29,6 +26,7 @@ class PcoTestConsumerMixin(CustomDetectorMixin): Parent class: CustomDetectorMixin """ + # pylint: disable=protected-access def on_stage(self): """Start listening for preview data stream""" if self.parent._mon is not None: @@ -72,29 +70,27 @@ class PcoTestConsumerMixin(CustomDetectorMixin): t_elapsed = t_curr - t_last if t_elapsed < self.parent.throttle.get(): continue - """ - # Unpack the Array V1 reply to metadata and array data - meta, data = r - print(meta) + # # Unpack the Array V1 reply to metadata and array data + # meta, data = r + # print(meta) - # Update image and update subscribers - header = json.loads(meta) - if header["type"] == "uint16": - image = np.frombuffer(data, dtype=np.uint16) - if image.size != np.prod(header['shape']): - err = f"Unexpected array size of {image.size} for header: {header}" - raise ValueError(err) - image = image.reshape(header['shape']) + # # Update image and update subscribers + # header = json.loads(meta) + # if header["type"] == "uint16": + # image = np.frombuffer(data, dtype=np.uint16) + # if image.size != np.prod(header['shape']): + # err = f"Unexpected array size of {image.size} for header: {header}" + # raise ValueError(err) + # image = image.reshape(header['shape']) - # Update image and update subscribers - self.parent.frame.put(header['frame'], force=True) - self.parent.image_shape.put(header['shape'], force=True) - self.parent.image.put(image, force=True) - self.parent._last_image = image - self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image) - """ + # # Update image and update subscribers + # self.parent.frame.put(header['frame'], force=True) + # self.parent.image_shape.put(header['shape'], force=True) + # self.parent.image.put(image, force=True) + # self.parent._last_image = image + # self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image) t_last = t_curr - #logger.info( + # logger.info( # f"[{self.parent.name}] Updated frame {header['frame']}\t" # f"Shape: {header['shape']}\tMean: {np.mean(image):.3f}" # ) @@ -110,7 +106,7 @@ class PcoTestConsumerMixin(CustomDetectorMixin): finally: try: self.parent._socket.disconnect() - except: + except RuntimeError: pass self.parent._mon = None logger.info(f"[{self.parent.name}]\tDetaching monitor") @@ -126,6 +122,7 @@ class PcoTestConsumer(PSIDetectorBase): You can add a preview widget to the dock by: cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1') """ + # Subscriptions for plotting image USER_ACCESS = ["get_last_image"] SUB_MONITOR = "device_monitor_2d" @@ -171,8 +168,7 @@ class PcoTestConsumer(PSIDetectorBase): self._socket.connect(self.url.get()) def disconnect(self): - """Disconnect - """ + """Disconnect""" try: if self._socket is not None: self._socket.disconnect(self.url.get()) @@ -181,7 +177,6 @@ class PcoTestConsumer(PSIDetectorBase): finally: self._socket = None - def get_image(self): return self._last_image diff --git a/tomcat_bec/devices/gigafrost/pcoedgecamera.py b/tomcat_bec/devices/gigafrost/pcoedgecamera.py index 32899e0..0f046a6 100644 --- a/tomcat_bec/devices/gigafrost/pcoedgecamera.py +++ b/tomcat_bec/devices/gigafrost/pcoedgecamera.py @@ -4,13 +4,12 @@ Created on Wed Dec 6 11:33:54 2023 @author: mohacsi_i """ - +import time from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind from ophyd.status import SubscriptionStatus, DeviceStatus -import time -from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase as PSIDeviceBase +from ophyd_devices import BECDeviceBase from ophyd_devices.interfaces.base_classes.psi_detector_base import ( - CustomDetectorMixin as CustomDeviceMixin, + CustomDetectorMixin as CustomPrepare, ) try: @@ -23,12 +22,12 @@ except ModuleNotFoundError: logger = logging.getLogger("PcoEdgeCam") -class PcoEdgeCameraMixin(CustomDeviceMixin): +class PcoEdgeCameraMixin(CustomPrepare): """Mixin class to setup the Helge camera bae class. This class will be called by the custom_prepare_cls attribute of the detector class. """ - + # pylint: disable=protected-access def on_stage(self) -> None: """Configure and arm PCO.Edge camera for acquisition""" @@ -91,7 +90,6 @@ class PcoEdgeCameraMixin(CustomDeviceMixin): 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. """ - logger.warning(f"triggering PCO") # Ensure that previous data transfer finished # def sentIt(*args, value, timestamp, **kwargs): # return value==0 @@ -99,11 +97,11 @@ class PcoEdgeCameraMixin(CustomDeviceMixin): # status.wait() # Not sure if it always sends the first batch of images or the newest - def didWeReset(*args, old_value, value, timestamp, **kwargs): + 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, didWeReset, timeout=5) + status = SubscriptionStatus(self.parent.buffer_used, wait_bufferreset, timeout=5) status.wait() t_expected = ( @@ -111,13 +109,13 @@ class PcoEdgeCameraMixin(CustomDeviceMixin): ) * self.parent.file_savestop.get() # Wait until the buffer fills up with enough images - def areWeDoneYet(*args, old_value, value, timestamp, **kwargs): + 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, areWeDoneYet, timeout=max(5, 5 * t_expected), settle_time=0.2 + self.parent.buffer_used, wait_acquisition, timeout=max_wait, settle_time=0.2 ) status.wait() @@ -130,19 +128,18 @@ class PcoEdgeCameraMixin(CustomDeviceMixin): # against values from the previous cycle, i.e. pass automatically. t_start = time.time() - def haveWeSentIt(*args, old_value, value, timestamp, **kwargs): + 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, haveWeSentIt, timeout=120, settle_time=0.2 + self.parent.file_savebusy, wait_sending, timeout=120, settle_time=0.2 ) status.wait() - logger.warning(f"done PCO") -class HelgeCameraBase(PSIDeviceBase): +class HelgeCameraBase(BECDeviceBase): """Ophyd baseclass for Helge camera IOCs This class provides wrappers for Helge's camera IOCs around SwissFEL and @@ -161,19 +158,21 @@ class HelgeCameraBase(PSIDeviceBase): UPDATE: Data sending operation modes - Switch to ZMQ streaming by setting FILEFORMAT to ZEROMQ, set SAVESTART and SAVESTOP to select a ROI of images and start file transfer with FTRANSFER. - The ZMQ connection streams out the data in PUSH-PULL mode, i.e. it needs incoming connection. + - 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 and SAVESTART and SAVESTOP selects a ROI of images to be streamed + 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 and SAVESTART and SAVESTOP selects a ROI of images 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 - - + 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 """ # ######################################################################## @@ -274,7 +273,7 @@ class HelgeCameraBase(PSIDeviceBase): @state.setter def state(self): - raise ReadOnlyError("State is a ReadOnly property") + raise RuntimeError("State is a ReadOnly property") def configure(self, d: dict = {}) -> tuple: """Configure the base Helge camera device @@ -323,11 +322,11 @@ class HelgeCameraBase(PSIDeviceBase): # So we need a 'negedge' on SET_PARAM self.camSetParam.set(1).wait() - def fallingEdge(*args, old_value, value, timestamp, **kwargs): + def negedge(*, old_value, value, timestamp, **_): return bool(old_value and not value) # Subscribe and wait for update - status = SubscriptionStatus(self.camSetParam, fallingEdge, timeout=5, settle_time=0.5) + status = SubscriptionStatus(self.camSetParam, negedge, timeout=5, settle_time=0.5) status.wait() def bluestage(self): @@ -341,24 +340,23 @@ class HelgeCameraBase(PSIDeviceBase): self.bufferStoreMode.get() in ("Recorder", 0) and self.file_savestop.get() > self.buffer_size.get() ): - self.logger.warning( - "You're about to send some empty images, {self.file_savestop.get()} is above buffer size" + logger.warning( + f"You'll send empty images, {self.file_savestop.get()} is above buffer size" ) # 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): + def is_running(*, value, timestamp, **_): return bool(value == 6) - status = SubscriptionStatus(self.camStatusCode, isRunning, timeout=5, settle_time=0.2) + status = SubscriptionStatus(self.camStatusCode, is_running, 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 # Data streaming is stopped by setting the max index to 0 # FIXME: This might interrupt data transfer @@ -371,13 +369,6 @@ class HelgeCameraBase(PSIDeviceBase): """ self.file_transfer.set(1).wait() - # def complete(self): - # """ Wait until the images have been sent""" - # def areWeSending(*args, value, timestamp, **kwargs): - # return not bool(value) - # status = SubscriptionStatus(self.file_savebusy, haveWeSentIt, timeout=None, settle_time=0.2) - # return status - class PcoEdge5M(HelgeCameraBase): """Ophyd baseclass for PCO.Edge cameras @@ -402,7 +393,8 @@ class PcoEdge5M(HelgeCameraBase): 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) + # acqTriggerSource = Component( + # EpicsSignalRO, "TRIGGERSOURCE", auto_monitor=True, kind=Kind.config) # acqTriggerEdge = Component(EpicsSignalRO, "TRIGGEREDGE", auto_monitor=True, kind=Kind.config) # ######################################################################## @@ -465,9 +457,9 @@ class PcoEdge5M(HelgeCameraBase): 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() + self.bin_x.set(d["image_binx"]).wait() if "image_biny" in d and d["image_biny"] is not None: - self.pxBinY.set(d["image_biny"]).wait() + self.bin_y.set(d["image_biny"]).wait() # Call super() to commit the changes super().configure(d) @@ -477,5 +469,5 @@ class PcoEdge5M(HelgeCameraBase): if __name__ == "__main__": # Drive data collection - cam = PcoEdgeBase("X02DA-CCDCAM2:", name="mcpcam") + cam = PcoEdge5M("X02DA-CCDCAM2:", name="mcpcam") cam.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/stddaq_client.py b/tomcat_bec/devices/gigafrost/stddaq_client.py index fc4603f..34006b2 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_client.py +++ b/tomcat_bec/devices/gigafrost/stddaq_client.py @@ -12,25 +12,26 @@ from threading import Thread import requests import os -from ophyd import Device, Signal, Component, Kind, Staged +from ophyd import Signal, Component, Kind from ophyd.status import SubscriptionStatus -from ophyd.flyers import FlyerInterface from websockets.sync.client import connect, ClientConnection from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError 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, +) from bec_lib import bec_logger -from bec_lib.file_utils import FileWriter + logger = bec_logger.logger class StdDaqMixin(CustomDeviceMixin): - # parent : StdDaqClient + # pylint: disable=protected-access _mon = None def on_stage(self) -> None: - """ Configuration and staging + """Configuration and staging In the BEC model ophyd devices must fish out their own configuration from the 'scaninfo'. I.e. they need to know which parameters are relevant for them at each scan. @@ -40,30 +41,30 @@ class StdDaqMixin(CustomDeviceMixin): # Fish out our configuration from scaninfo (via explicit or generic addressing) # NOTE: Scans don't have to fully configure the device d = {} - if 'kwargs' in self.parent.scaninfo.scan_msg.info: - scanargs = self.parent.scaninfo.scan_msg.info['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 'nr_writers' in scanargs and scanargs['nr_writers'] != None: - d['nr_writers'] = scanargs['nr_writers'] - if 'file_path' in scanargs and scanargs['file_path']!=None: - self.parent.file_path.set(scanargs['file_path'].replace('data','gpfs')).wait() - print(scanargs['file_path']) - if os.path.isdir(scanargs['file_path']): + if "kwargs" in self.parent.scaninfo.scan_msg.info: + scanargs = self.parent.scaninfo.scan_msg.info["kwargs"] + 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 "nr_writers" in scanargs and scanargs["nr_writers"] is not None: + d["nr_writers"] = scanargs["nr_writers"] + if "file_path" in scanargs and scanargs["file_path"] is not None: + self.parent.file_path.set(scanargs["file_path"].replace("data", "gpfs")).wait() + print(scanargs["file_path"]) + if os.path.isdir(scanargs["file_path"]): print("isdir") pass else: print("creating") try: - os.makedirs(scanargs['file_path'], 0o777) - os.system('chmod -R 777 ' + scanargs['base_path']) + os.makedirs(scanargs["file_path"], 0o777) + os.system("chmod -R 777 " + scanargs["base_path"]) except: - print('Problem with creating folder') - if 'file_prefix' in scanargs and scanargs['file_prefix']!=None: - print(scanargs['file_prefix']) - self.parent.file_prefix.set(scanargs['file_prefix']).wait() + print("Problem with creating folder") + if "file_prefix" in scanargs and scanargs["file_prefix"] != None: + print(scanargs["file_prefix"]) + self.parent.file_prefix.set(scanargs["file_prefix"]).wait() if "daq_num_points" in scanargs: d["num_points_total"] = scanargs["daq_num_points"] @@ -71,13 +72,13 @@ class StdDaqMixin(CustomDeviceMixin): # Try to figure out number of points num_points = 1 points_valid = False - if "steps" in scanargs and scanargs['steps'] is not None: + if "steps" in scanargs and scanargs["steps"] is not None: num_points *= scanargs["steps"] points_valid = True - if "exp_burst" in scanargs and scanargs['exp_burst'] is not None: + if "exp_burst" in scanargs and scanargs["exp_burst"] is not None: num_points *= scanargs["exp_burst"] points_valid = True - if "repeats" in scanargs and scanargs['repeats'] is not None: + if "repeats" in scanargs and scanargs["repeats"] is not None: num_points *= scanargs["repeats"] points_valid = True if points_valid: @@ -98,19 +99,17 @@ class StdDaqMixin(CustomDeviceMixin): self._mon.start() def on_unstage(self): - """ Stop a running acquisition and close connection - """ + """Stop a running acquisition and close connection""" print("Creating virtual dataset") self.parent.create_virtual_dataset() self.parent.blueunstage() def on_stop(self): - """ Stop a running acquisition and close connection - """ + """Stop a running acquisition and close connection""" self.parent.blueunstage() def monitor(self) -> None: - """ Monitor status messages while connection is open. This will block the reply monitoring + """Monitor status messages while connection is open. This will block the reply monitoring to calling unstage() might throw. Status updates are sent every 1 seconds, but finishing acquisition means StdDAQ will close connection, so there's no idle state polling. """ @@ -144,19 +143,31 @@ class StdDaqClient(PSIDeviceBase): daq = StdDaqClient(name="daq", ws_url="ws://xbl-daq-29:8080", rest_url="http://xbl-daq-29:5000") ``` """ + # pylint: disable=too-many-instance-attributes custom_prepare_cls = StdDaqMixin - USER_ACCESS = ["set_daq_config", "get_daq_config", "nuke", "connect", "message", "state", "bluestage", "blueunstage"] + USER_ACCESS = [ + "set_daq_config", + "get_daq_config", + "nuke", + "connect", + "message", + "state", + "bluestage", + "blueunstage", + ] _wsclient = None # Status attributes - ws_url = Component(Signal, kind=Kind.config, metadata={'write_access': False}) - runstatus = Component(Signal, value="unknown", kind=Kind.normal, metadata={'write_access': False}) + ws_url = Component(Signal, kind=Kind.config, metadata={"write_access": False}) + runstatus = Component( + Signal, value="unknown", kind=Kind.normal, metadata={"write_access": False} + ) num_images = Component(Signal, value=10000, kind=Kind.config) file_path = Component(Signal, value="/gpfs/test/test-beamline", kind=Kind.config) file_prefix = Component(Signal, value="file", kind=Kind.config) - # Configuration attributes - rest_url = Component(Signal, kind=Kind.config, metadata={'write_access': False}) + # Configuration attributes + rest_url = Component(Signal, kind=Kind.config, metadata={"write_access": False}) cfg_detector_name = Component(Signal, kind=Kind.config) cfg_detector_type = Component(Signal, kind=Kind.config) cfg_bit_depth = Component(Signal, kind=Kind.config) @@ -176,10 +187,19 @@ class StdDaqClient(PSIDeviceBase): device_manager=None, ws_url: str = "ws://localhost:8080", rest_url: str = "http://localhost:5000", - data_source_name = None, + data_source_name=None, **kwargs, ) -> None: - super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, device_manager=device_manager, **kwargs) + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + device_manager=device_manager, + **kwargs, + ) self.ws_url.set(ws_url, force=True).wait() self.rest_url.set(rest_url, force=True).wait() self.data_source_name = data_source_name @@ -264,47 +284,49 @@ class StdDaqClient(PSIDeviceBase): """ # Configuration parameters - if 'image_width' in d and d['image_width']!=None: - self.cfg_pixel_width.set(d['image_width']).wait() - if 'image_height' in d and d['image_height']!=None: - self.cfg_pixel_height.set(d['image_height']).wait() - if 'bit_depth' in d: - self.cfg_bit_depth.set(d['bit_depth']).wait() - if 'nr_writers' in d and d['nr_writers']!=None: - self.cfg_nr_writers.set(d['nr_writers']).wait() + if "image_width" in d and d["image_width"] != None: + self.cfg_pixel_width.set(d["image_width"]).wait() + if "image_height" in d and d["image_height"] != None: + self.cfg_pixel_height.set(d["image_height"]).wait() + if "bit_depth" in d: + self.cfg_bit_depth.set(d["bit_depth"]).wait() + if "nr_writers" in d and d["nr_writers"] != None: + self.cfg_nr_writers.set(d["nr_writers"]).wait() # Run parameters - if 'num_points_total' in d: - self.num_images.set(d['num_points_total']).wait() + if "num_points_total" in d: + self.num_images.set(d["num_points_total"]).wait() # Restart the DAQ if resolution changed cfg = self.get_daq_config() - if cfg['image_pixel_height'] != self.cfg_pixel_height.get() or \ - cfg['image_pixel_width'] != self.cfg_pixel_width.get() or \ - cfg['bit_depth'] != self.cfg_bit_depth.get() or \ - cfg['number_of_writers'] != self.cfg_nr_writers.get(): - + if ( + cfg["image_pixel_height"] != self.cfg_pixel_height.get() + or cfg["image_pixel_width"] != self.cfg_pixel_width.get() + or cfg["bit_depth"] != self.cfg_bit_depth.get() + or cfg["number_of_writers"] != self.cfg_nr_writers.get() + ): + # Stop if current status is not idle if self.state() != "idle": logger.warning(f"[{self.name}] stdDAQ reconfiguration might corrupt files") # Update retrieved config - cfg['image_pixel_height'] = int(self.cfg_pixel_height.get()) - cfg['image_pixel_width'] = int(self.cfg_pixel_width.get()) - cfg['bit_depth'] = int(self.cfg_bit_depth.get()) - cfg['number_of_writers'] = int(self.cfg_nr_writers.get()) + cfg["image_pixel_height"] = int(self.cfg_pixel_height.get()) + cfg["image_pixel_width"] = int(self.cfg_pixel_width.get()) + cfg["bit_depth"] = int(self.cfg_bit_depth.get()) + cfg["number_of_writers"] = int(self.cfg_nr_writers.get()) self.set_daq_config(cfg) sleep(1) self.get_daq_config(update=True) def bluestage(self): - """ Stages the stdDAQ + """Stages the stdDAQ - Opens a new connection to the stdDAQ, sends the start command with - the current configuration. It waits for the first reply and checks - it for obvious failures. + Opens a new connection to the stdDAQ, sends the start command with + the current configuration. It waits for the first reply and checks + it for obvious failures. """ # Can't stage into a running exposure - if self.state() != 'idle': + if self.state() != "idle": raise RuntimeError(f"[{self.name}] stdDAQ can't stage from state: {self.state()}") # Must make sure that image size matches the data source @@ -315,10 +337,13 @@ class StdDaqClient(PSIDeviceBase): daq_img_h = self.cfg_pixel_height.get() if not (daq_img_w == cam_img_w and daq_img_h == cam_img_h): - raise RuntimeError(f"[{self.name}] stdDAQ image resolution ({daq_img_w} , {daq_img_h}) does not match camera with ({cam_img_w} , {cam_img_h})") + raise RuntimeError( + f"[{self.name}] stdDAQ image resolution ({daq_img_w} , {daq_img_h}) does not match camera with ({cam_img_w} , {cam_img_h})" + ) else: - logger.warning(f"[{self.name}] stdDAQ image resolution ({daq_img_w} , {daq_img_h}) matches camera with ({cam_img_w} , {cam_img_h})") - + logger.warning( + f"[{self.name}] stdDAQ image resolution ({daq_img_w} , {daq_img_h}) matches camera with ({cam_img_w} , {cam_img_h})" + ) file_path = self.file_path.get() num_images = self.num_images.get() @@ -327,7 +352,12 @@ class StdDaqClient(PSIDeviceBase): # New connection self._wsclient = self.connect() - message = {"command": "start", "path": file_path, "file_prefix": file_prefix, "n_image": num_images, } + message = { + "command": "start", + "path": file_path, + "file_prefix": file_prefix, + "n_image": num_images, + } reply = self.message(message) if reply is not None: @@ -338,8 +368,10 @@ class StdDaqClient(PSIDeviceBase): # Give it more time to reconfigure if reply["status"] in ("rejected"): # FIXME: running exposure is a nogo - if reply['reason'] == "driver is busy!": - raise RuntimeError(f"[{self.name}] Start stdDAQ command rejected: already running") + if reply["reason"] == "driver is busy!": + raise RuntimeError( + f"[{self.name}] Start stdDAQ command rejected: already running" + ) else: # Give it more time to consolidate sleep(1) @@ -348,16 +380,18 @@ class StdDaqClient(PSIDeviceBase): print(f"[{self.name}] Started stdDAQ in: {reply['status']}") return - raise RuntimeError(f"[{self.name}] Failed to start the stdDAQ in 1 tries, reason: {reply['reason']}") + raise RuntimeError( + f"[{self.name}] Failed to start the stdDAQ in 1 tries, reason: {reply['reason']}" + ) def blueunstage(self): - """ Unstages the stdDAQ + """Unstages the stdDAQ - Opens a new connection to the stdDAQ, sends the stop command and - waits for the idle state. + Opens a new connection to the stdDAQ, sends the stop command and + waits for the idle state. """ ii = 0 - while ii<10: + while ii < 10: # Stop the DAQ (will close connection) - reply is always "success" self._wsclient = self.connect() self.message({"command": "stop_all"}, wait_reply=False) @@ -371,7 +405,7 @@ class StdDaqClient(PSIDeviceBase): if reply is not None: logger.info(f"[{self.name}] DAQ status reply: {reply}") reply = json.loads(reply) - + if reply["status"] in ("idle", "error"): # Only 'idle' state accepted print(f"DAQ stopped on try {ii}") @@ -388,6 +422,7 @@ class StdDaqClient(PSIDeviceBase): # Bluesky flyer interface def complete(self) -> SubscriptionStatus: """Wait for current run. Must end in status 'file_saved'.""" + def is_running(*args, value, timestamp, **kwargs): result = value in ["idle", "file_saved", "error"] return result @@ -396,40 +431,35 @@ class StdDaqClient(PSIDeviceBase): return status def get_daq_config(self, update=False) -> dict: - """Read the current configuration from the DAQ - """ - r = requests.get( - self.rest_url.get() + '/api/config/get', - params={'user': "ioc"}, - timeout=2) + """Read the current configuration from the DAQ""" + r = requests.get(self.rest_url.get() + "/api/config/get", params={"user": "ioc"}, timeout=2) if r.status_code != 200: raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") cfg = r.json() if update: - self.cfg_detector_name.set(cfg['detector_name']).wait() - self.cfg_detector_type.set(cfg['detector_type']).wait() - self.cfg_bit_depth.set(cfg['bit_depth']).wait() - self.cfg_pixel_height.set(cfg['image_pixel_height']).wait() - self.cfg_pixel_width.set(cfg['image_pixel_width']).wait() - self.cfg_nr_writers.set(cfg['number_of_writers']).wait() + self.cfg_detector_name.set(cfg["detector_name"]).wait() + self.cfg_detector_type.set(cfg["detector_type"]).wait() + self.cfg_bit_depth.set(cfg["bit_depth"]).wait() + self.cfg_pixel_height.set(cfg["image_pixel_height"]).wait() + self.cfg_pixel_width.set(cfg["image_pixel_width"]).wait() + self.cfg_nr_writers.set(cfg["number_of_writers"]).wait() return cfg def set_daq_config(self, config, settle_time=1): - """Write a full configuration to the DAQ - """ - url = self.rest_url.get() + '/api/config/set' + """Write a full configuration to the DAQ""" + url = self.rest_url.get() + "/api/config/set" r = requests.post( - url, - params={"user": "ioc"}, - json=config, - timeout=2, - headers={"Content-Type": "application/json"} - ) + url, + params={"user": "ioc"}, + json=config, + timeout=2, + headers={"Content-Type": "application/json"}, + ) if r.status_code != 200: raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") # Wait for service to restart (and connect to make sure) - #sleep(settle_time) + # sleep(settle_time) self.connect() return r.json() @@ -437,20 +467,24 @@ class StdDaqClient(PSIDeviceBase): """Combine the stddaq written files in a given folder in an interleaved h5 virtual dataset """ - url = self.rest_url.get() + '/api/h5/create_interleaved_vds' + url = self.rest_url.get() + "/api/h5/create_interleaved_vds" file_path = self.file_path.get() file_prefix = self.file_prefix.get() - + r = requests.post( url, - params = {'user': 'ioc'}, - json = {'base_path': file_path, 'file_prefix': file_prefix, 'output_file': file_prefix.rstrip('_') + '.h5'}, - timeout = 2, - headers = {'Content-type': 'application/json'} + params={"user": "ioc"}, + json={ + "base_path": file_path, + "file_prefix": file_prefix, + "output_file": file_prefix.rstrip("_") + ".h5", + }, + timeout=2, + headers={"Content-type": "application/json"}, ) def nuke(self, restarttime=5): - """ Reconfigures the stdDAQ to restart the services. This causes + """Reconfigures the stdDAQ to restart the services. This causes systemd to kill the current DAQ service and restart it with the same configuration. Which might corrupt the currently written file... """ @@ -459,18 +493,20 @@ class StdDaqClient(PSIDeviceBase): sleep(restarttime) def state(self) -> str | None: - """ Querry the current system status""" + """Querry the current system status""" try: wsclient = self.connect() - wsclient.send(json.dumps({'command': 'status'})) + wsclient.send(json.dumps({"command": "status"})) r = wsclient.recv(timeout=1) r = json.loads(r) - return r['status'] + return r["status"] except ConnectionRefusedError: raise # Automatically connect to microXAS testbench if directly invoked if __name__ == "__main__": - daq = StdDaqClient(name="daq", ws_url="ws://sls-daq-001:8080", rest_url="http://sls-daq-001:5000") + daq = StdDaqClient( + name="daq", ws_url="ws://sls-daq-001:8080", rest_url="http://sls-daq-001:5000" + ) daq.wait_for_connection() -- 2.49.1