From 163ef3c7a5e9c0195ee37d82b774ac3236e392c8 Mon Sep 17 00:00:00 2001 From: gac-x05la Date: Thu, 13 Feb 2025 18:16:42 +0100 Subject: [PATCH] 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(