diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml index 7edb0a3..a759856 100644 --- a/tomcat_bec/device_configs/microxas_test_bed.yaml +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -49,9 +49,9 @@ femto_mean_curr: softwareTrigger: false es1_roty: - readoutPriority: baseline + readoutPriority: monitored description: 'Test rotation stage' - deviceClass: tomcat_bec.devices.psimotor.EpicsMotorMR + deviceClass: ophyd.EpicsMotor deviceConfig: prefix: X02DA-ES1-SMP1:ROTY deviceTags: @@ -61,59 +61,59 @@ es1_roty: readOnly: false softwareTrigger: false -es1_ismc: - description: 'Automation1 iSMC interface' - deviceClass: tomcat_bec.devices.aa1Controller - deviceConfig: - prefix: 'X02DA-ES1-SMP1:CTRL:' - deviceTags: - - es1 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: false +# es1_ismc: +# description: 'Automation1 iSMC interface' +# deviceClass: tomcat_bec.devices.aa1Controller +# deviceConfig: +# prefix: 'X02DA-ES1-SMP1:CTRL:' +# deviceTags: +# - es1 +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: monitored +# softwareTrigger: false -es1_tasks: - description: 'Automation1 task management interface' - deviceClass: tomcat_bec.devices.aa1Tasks - deviceConfig: - prefix: 'X02DA-ES1-SMP1:TASK:' - deviceTags: - - es1 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: false +# es1_tasks: +# description: 'Automation1 task management interface' +# deviceClass: tomcat_bec.devices.aa1Tasks +# deviceConfig: +# prefix: 'X02DA-ES1-SMP1:TASK:' +# deviceTags: +# - es1 +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: monitored +# softwareTrigger: false -es1_psod: - description: 'AA1 PSO output interface (trigger)' - deviceClass: tomcat_bec.devices.aa1AxisPsoDistance - deviceConfig: - prefix: 'X02DA-ES1-SMP1:ROTY:PSO:' - deviceTags: - - es1 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: true +# es1_psod: +# description: 'AA1 PSO output interface (trigger)' +# deviceClass: tomcat_bec.devices.aa1AxisPsoDistance +# deviceConfig: +# prefix: 'X02DA-ES1-SMP1:ROTY:PSO:' +# deviceTags: +# - es1 +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: monitored +# softwareTrigger: true -es1_ddaq: - description: 'Automation1 position recording interface' - deviceClass: tomcat_bec.devices.aa1AxisDriveDataCollection - deviceConfig: - prefix: 'X02DA-ES1-SMP1:ROTY:DDC:' - deviceTags: - - es1 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: monitored - softwareTrigger: false +# es1_ddaq: +# description: 'Automation1 position recording interface' +# deviceClass: tomcat_bec.devices.aa1AxisDriveDataCollection +# deviceConfig: +# prefix: 'X02DA-ES1-SMP1:ROTY:DDC:' +# deviceTags: +# - es1 +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: monitored +# softwareTrigger: false #camera: @@ -151,6 +151,7 @@ gfdaq: deviceConfig: ws_url: 'ws://129.129.95.111:8080' rest_url: 'http://129.129.95.111:5000' + data_source_name: 'gfcam' deviceTags: - std-daq enabled: true diff --git a/tomcat_bec/device_configs/microxas_test_bed_tmp.yaml b/tomcat_bec/device_configs/microxas_test_bed_tmp.yaml new file mode 100644 index 0000000..7edb0a3 --- /dev/null +++ b/tomcat_bec/device_configs/microxas_test_bed_tmp.yaml @@ -0,0 +1,186 @@ +eyex: + readoutPriority: baseline + description: X-ray eye axis X + deviceClass: tomcat_bec.devices.psimotor.EpicsMotorEC + deviceConfig: + prefix: MTEST-X05LA-ES2-XRAYEYE:M1 + deviceTags: + - xray-eye + onFailure: buffer + 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 +femto_mean_curr: + readoutPriority: monitored + description: Femto mean current + deviceClass: ophyd.EpicsSignal + deviceConfig: + auto_monitor: true + read_pv: MTEST-X05LA-ES2-XRAYEYE:FEMTO-MEAN-CURR + deviceTags: + - xray-eye + onFailure: buffer + enabled: true + readOnly: true + softwareTrigger: false + +es1_roty: + readoutPriority: baseline + description: 'Test rotation stage' + deviceClass: tomcat_bec.devices.psimotor.EpicsMotorMR + deviceConfig: + prefix: X02DA-ES1-SMP1:ROTY + deviceTags: + - es1-sam + onFailure: buffer + enabled: true + readOnly: false + softwareTrigger: false + +es1_ismc: + description: 'Automation1 iSMC interface' + deviceClass: tomcat_bec.devices.aa1Controller + deviceConfig: + prefix: 'X02DA-ES1-SMP1:CTRL:' + deviceTags: + - es1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: false + +es1_tasks: + description: 'Automation1 task management interface' + deviceClass: tomcat_bec.devices.aa1Tasks + deviceConfig: + prefix: 'X02DA-ES1-SMP1:TASK:' + deviceTags: + - es1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: false + + +es1_psod: + description: 'AA1 PSO output interface (trigger)' + deviceClass: tomcat_bec.devices.aa1AxisPsoDistance + deviceConfig: + prefix: 'X02DA-ES1-SMP1:ROTY:PSO:' + deviceTags: + - es1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: true + + +es1_ddaq: + description: 'Automation1 position recording interface' + deviceClass: tomcat_bec.devices.aa1AxisDriveDataCollection + deviceConfig: + prefix: 'X02DA-ES1-SMP1:ROTY:DDC:' + deviceTags: + - es1 + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: false + + +#camera: +# description: Grashopper Camera +# deviceClass: tomcat_bec.devices.GrashopperTOMCAT +# deviceConfig: +# prefix: 'X02DA-PG-USB:' +# deviceTags: +# - camera +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: monitored +# softwareTrigger: true + +gfcam: + description: GigaFrost camera client + deviceClass: tomcat_bec.devices.GigaFrostCamera + deviceConfig: + prefix: 'X02DA-CAM-GF2:' + backend_url: 'http://sls-daq-001:8080' + auto_soft_enable: true + deviceTags: + - camera + - trigger + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: true + +gfdaq: + description: GigaFrost stdDAQ client + deviceClass: tomcat_bec.devices.StdDaqClient + deviceConfig: + ws_url: 'ws://129.129.95.111:8080' + rest_url: 'http://129.129.95.111:5000' + deviceTags: + - std-daq + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: false + +daq_stream0: + description: stdDAQ preview (2 every 555) + deviceClass: tomcat_bec.devices.StdDaqPreviewDetector + deviceConfig: + url: 'tcp://129.129.95.111:20000' + deviceTags: + - std-daq + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: false + +daq_stream1: + description: stdDAQ preview (1 at 5 Hz) + deviceClass: tomcat_bec.devices.StdDaqPreviewDetector + deviceConfig: + url: 'tcp://129.129.95.111:20001' + deviceTags: + - std-daq + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: false diff --git a/tomcat_bec/devices/aerotech/AerotechAutomation1.py b/tomcat_bec/devices/aerotech/AerotechAutomation1.py index 54ef57b..8d549a1 100644 --- a/tomcat_bec/devices/aerotech/AerotechAutomation1.py +++ b/tomcat_bec/devices/aerotech/AerotechAutomation1.py @@ -1,7 +1,11 @@ -from time import sleep +# -*- coding: utf-8 -*- +""" +Ophyd device for the Aerotech Automation1 IOC's generic interfaces + +@author: mohacsi_i +""" import numpy as np from ophyd import Component, Device, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind -from ophyd.status import DeviceStatus, SubscriptionStatus from bec_lib import bec_logger logger = bec_logger.logger @@ -38,8 +42,8 @@ class aa1Controller(Device): taskcount = Component(EpicsSignalRO, "TASKCOUNT", kind=Kind.config) fastpoll = Component(EpicsSignalRO, "POLLTIME", auto_monitor=True, kind=Kind.normal) slowpoll = Component(EpicsSignalRO, "DRVPOLLTIME", auto_monitor=True, kind=Kind.normal) - errno = Component(EpicsSignalRO, "ERRNO", auto_monitor=True, kind=Kind.hinted) - errnmsg = Component(EpicsSignalRO, "ERRMSG", auto_monitor=True, kind=Kind.hinted) + errno = Component(EpicsSignalRO, "ERRNO", auto_monitor=True, kind=Kind.normal) + errnmsg = Component(EpicsSignalRO, "ERRMSG", auto_monitor=True, kind=Kind.normal) _set_ismc = Component(EpicsSignal, "SET", put_complete=True, kind=Kind.omitted) USER_ACCESS = ["reset"] @@ -111,10 +115,10 @@ class aa1GlobalVariables(Device): if size is None or size == 0: self.integer_addr.set(address).wait() return self.integer_rb.get() - else: - self.integer_addr.set(address).wait() - self.integer_size.set(size).wait() - return self.integerarr_rb.get() + + self.integer_addr.set(address).wait() + self.integer_size.set(size).wait() + return self.integerarr_rb.get() def write_int(self, address: int, value, settle_time=0.1) -> None: """Write a 64-bit integer global variable @@ -150,10 +154,10 @@ class aa1GlobalVariables(Device): if size is None: self.real_addr.set(address).wait() return self.real_rb.get() - else: - self.real_addr.set(address).wait() - self.real_size.set(size).wait() - return self.realarr_rb.get() + + self.real_addr.set(address).wait() + self.real_size.set(size).wait() + return self.realarr_rb.get() def write_float(self, address: int, value, settle_time=0.1) -> None: """Write a 64-bit float global variable""" diff --git a/tomcat_bec/devices/aerotech/AerotechAutomation1Enums.py b/tomcat_bec/devices/aerotech/AerotechAutomation1Enums.py index 0b885ce..74710ff 100644 --- a/tomcat_bec/devices/aerotech/AerotechAutomation1Enums.py +++ b/tomcat_bec/devices/aerotech/AerotechAutomation1Enums.py @@ -1,5 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Enumerations for the Aerotech Automation1 controller as adopted from the Aerotech library. + +@author: mohacsi_i +""" from enum import Enum +# pylint: disable=missing-class-docstring +# pylint: disable=too-few-public-methods + class TomcatSequencerState: IDLE = 0 diff --git a/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py b/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py index c9f015e..b81ec3f 100644 --- a/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py +++ b/tomcat_bec/devices/aerotech/AerotechDriveDataCollection.py @@ -1,15 +1,16 @@ +# -*- coding: utf-8 -*- +""" +Ophyd device for the Aerotech Automation1 IOC's axis-specific synchronized +drive data collection (DDC) interface. + +@author: mohacsi_i +""" import time from collections import OrderedDict from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind from ophyd.status import SubscriptionStatus -try: - from AerotechAutomation1Enums import DriveDataCaptureInput, DriveDataCaptureTrigger -except ModuleNotFoundError: - from tomcat_bec.devices.aerotech.AerotechAutomation1Enums import DriveDataCaptureInput - from tomcat_bec.devices.aerotech.AerotechAutomation1Enums import DriveDataCaptureTrigger - 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, @@ -20,7 +21,7 @@ logger = bec_logger.logger class AerotechDriveDataCollectionMixin(CustomDeviceMixin): - """Configuration and staging + """Mixin class for self-configuration and staging NOTE: scripted scans start drive data collection internally """ @@ -29,23 +30,29 @@ class AerotechDriveDataCollectionMixin(CustomDeviceMixin): """Configuration and staging""" # Fish out configuration from scaninfo (does not need to be full configuration) - 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 "kwargs" in self.parent.scaninfo.scan_msg.info: + scanargs = self.parent.scaninfo.scan_msg.info["kwargs"] # NOTE: Scans don't have to fully configure the device if "ddc_trigger" in scanargs: d["ddc_trigger"] = scanargs["ddc_trigger"] - if "steps" in scanargs and "exp_burst" in scanargs: - scan_steps = scanargs["steps"] - scan_burst = scanargs["exp_burst"] - d["num_points_total"] = (scan_steps+1) * scan_burst - elif "exp_burst" in scanargs: - d["num_points_total"] = scanargs["exp_burst"] - elif "steps" in scanargs: - d["num_points_total"] = scanargs["steps"] + if "ddc_num_points" in scanargs: + d["num_points_total"] = scanargs["ddc_num_points"] + else: + # Try to figure out number of points + num_points = 1 + points_valid = False + if "steps" in scanargs and scanargs['steps'] is not None: + num_points *= scanargs["steps"] + points_valid = True + elif "exp_burst" in scanargs and scanargs['exp_burst'] is not None: + num_points *= scanargs["exp_burst"] + points_valid = True + elif "repeats" in scanargs and scanargs['repeats'] is not None: + num_points *= scanargs["repeats"] + points_valid = True + if points_valid: + d["num_points_total"] = num_points # Perform bluesky-style configuration if len(d) > 0: @@ -107,7 +114,7 @@ class aa1AxisDriveDataCollection(PSIDeviceBase): custom_prepare_cls = AerotechDriveDataCollectionMixin USER_ACCESS = ["configure", "reset"] - def configure(self, d: dict = {}) -> tuple: + def configure(self, d: dict = None) -> tuple: """Configure data capture Configures the hardware synchronized drive data capture (DDC) on an @@ -117,14 +124,15 @@ class aa1AxisDriveDataCollection(PSIDeviceBase): """ old = self.read_configuration() - if "num_points_total" in d: - self.npoints.set(d["num_points_total"]).wait() - if "ddc_trigger" in d: - self._trigger.set(d['ddc_trigger']).wait() - if "ddc_source0" in d: - self._input0.set(d['ddc_source0']).wait() - if "ddc_source1" in d: - self._input1.set(d['ddc_source1']).wait() + if d is not None: + if "num_points_total" in d: + self.npoints.set(d["num_points_total"]).wait() + if "ddc_trigger" in d: + self._trigger.set(d['ddc_trigger']).wait() + if "ddc_source0" in d: + self._input0.set(d['ddc_source0']).wait() + if "ddc_source1" in d: + self._input1.set(d['ddc_source1']).wait() # Reset incremental readback self._switch.set("ResetRB", settle_time=0.1).wait() @@ -150,17 +158,17 @@ class aa1AxisDriveDataCollection(PSIDeviceBase): # Define wait until the busy flag goes down (excluding initial update) timestamp_ = 0 - def negEdge(*args, old_value, value, timestamp, **kwargs): + def neg_edge(*args, old_value, value, timestamp, **kwargs): nonlocal timestamp_ result = False if (timestamp_ == 0) else (old_value == 1 and value == 0) timestamp_ = timestamp return result if index == 0: - status = SubscriptionStatus(self._readstatus0, negEdge, settle_time=0.5) + status = SubscriptionStatus(self._readstatus0, neg_edge, settle_time=0.5) self._readback0.set(1).wait() elif index == 1: - status = SubscriptionStatus(self._readstatus1, negEdge, settle_time=0.5) + status = SubscriptionStatus(self._readstatus1, neg_edge, settle_time=0.5) self._readback1.set(1).wait() # Start asynchronous readback @@ -168,6 +176,8 @@ class aa1AxisDriveDataCollection(PSIDeviceBase): return status def describe_collect(self) -> OrderedDict: + """Describes collected array format according to JSONschema + """ ret = OrderedDict() ret["buffer0"] = { "source": "internal", diff --git a/tomcat_bec/devices/aerotech/AerotechPso.py b/tomcat_bec/devices/aerotech/AerotechPso.py index 678818e..9202393 100644 --- a/tomcat_bec/devices/aerotech/AerotechPso.py +++ b/tomcat_bec/devices/aerotech/AerotechPso.py @@ -1,3 +1,10 @@ +# -*- coding: utf-8 -*- +""" +Ophyd device for the Aerotech Automation1 IOC's axis-specific position +synchronized output (PSO) interface. + +@author: mohacsi_i +""" from time import sleep import numpy as np from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind @@ -12,6 +19,8 @@ logger = bec_logger.logger class AerotechPsoDistanceMixin(CustomDeviceMixin): + """Mixin class for self-configuration and staging + """ # parent : aa1Tasks def on_stage(self) -> None: """Configuration and staging @@ -21,14 +30,10 @@ class AerotechPsoDistanceMixin(CustomDeviceMixin): when not in use. I.e. this method is not expected to be called when PSO is not needed or when it'd conflict with other devices. """ - # Fish out configuration from scaninfo (does not need to be full configuration) - 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 "kwargs" in self.parent.scaninfo.scan_msg.info: + scanargs = self.parent.scaninfo.scan_msg.info["kwargs"] if "pso_distance" in scanargs: d["pso_distance"] = scanargs["pso_distance"] if "pso_wavemode" in scanargs: @@ -237,7 +242,7 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): """Simplified configuration interface to access the most common functionality for distance mode PSO. - :param pso_distance: The trigger distance or the array of distances between subsequent points. + :param pso_distance: Distance or array of distances between subsequent trigger points. :param pso_wavemode: Waveform mode configuration, usually pulsed/toggled (default: pulsed). :param pso_t_pulse : trigger high duration in pulsed mode (default: 100 us) :param pso_w_pulse : trigger hold-off time in pulsed mode (default: 200 us) @@ -276,7 +281,7 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): new = self.read_configuration() logger.info(f"[{self.name}] PSO configured to {pso_wavemode} mode") return (old, new) - + def bluestage(self) -> None: """Bluesky style stage""" # Stage the PSO distance module and zero counter @@ -288,25 +293,3 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): if self.dstDistanceVal.get() > 0: self.dstEventsEna.set("On").wait() self.dstCounterEna.set("On").wait() - - - # # ######################################################################## - # # Bluesky flyer interface - # def prepare(self, distance=None) -> DeviceStatus: - # """ Arm trigger for a synchronous or asynchronous acquisition""" - # if distance is not None: - # # Write a new array - # if isinstance(distance, (float, int)): - # self.dstDistance.set(distance).wait() - # elif isinstance(distance, (np.ndarray, list, tuple)): - # self.dstDistanceArr.set(distance).wait() - # else: - # # Rearm the already configured array - # if isinstance(self._distance_value, (np.ndarray, list, tuple)): - # self.dstArrayRearm.set(1).wait() - # # Start monitoring the counters - # self.dstEventsEna.set("On").wait() - # self.dstCounterEna.set("On").wait() - # status = DeviceStatus(self) - # status.set_finished() - # return status diff --git a/tomcat_bec/devices/aerotech/AerotechTasks.py b/tomcat_bec/devices/aerotech/AerotechTasks.py index 5b6e1c1..5e1b067 100644 --- a/tomcat_bec/devices/aerotech/AerotechTasks.py +++ b/tomcat_bec/devices/aerotech/AerotechTasks.py @@ -1,3 +1,10 @@ +# -*- coding: utf-8 -*- +""" +Ophyd device for the Aerotech Automation1 IOC's controller's task management +interface. + +@author: mohacsi_i +""" from time import sleep from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind from ophyd.status import DeviceStatus, SubscriptionStatus @@ -12,6 +19,8 @@ logger = bec_logger.logger class AerotechTasksMixin(CustomDeviceMixin): + """Mixin class for self-configuration and staging + """ # parent : aa1Tasks def on_stage(self) -> None: """Configuration and staging @@ -27,20 +36,16 @@ class AerotechTasksMixin(CustomDeviceMixin): # logger.warning(self.parent.scaninfo.scan_msg.info['kwargs'].keys()) # 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 "kwargs" in self.parent.scaninfo.scan_msg.info: + scanargs = self.parent.scaninfo.scan_msg.info["kwargs"] if self.parent.scaninfo.scan_type in ("script", "scripted"): # NOTE: Scans don't have to fully configure the device - if "script_text" in scanargs: + if "script_text" in scanargs and scanargs["script_text"] is not None: d["script_text"] = scanargs["script_text"] - if "script_file" in scanargs: + if "script_file" in scanargs and scanargs["script_file"] is not None: d["script_file"] = scanargs["script_file"] - if "script_task" in scanargs: + if "script_task" in scanargs and scanargs["script_task"] is not None: d["script_task"] = scanargs["script_task"] # Perform bluesky-style configuration @@ -114,7 +119,7 @@ class aa1Tasks(PSIDeviceBase): # Common operations old = self.read_configuration() self.switch.set("Reset").wait() - # Check if + # Check what we got if "script_task" in d: if d['script_task'] < 3 or d['script_task'] > 21: raise RuntimeError(f"Invalid task index: {d['script_task']}") @@ -136,7 +141,7 @@ class aa1Tasks(PSIDeviceBase): def bluestage(self) -> None: """Bluesky style stage""" if self.taskIndex.get() in (0, 1, 2): - logger.error(f"[{self.name}] Woah, launching AeroScript on a system task. Daring today are we?") + logger.error(f"[{self.name}] Launching AeroScript on system task. Daring today are we?") # Launch and check success status = self.switch.set("Run", settle_time=0.2) status.wait() @@ -151,13 +156,10 @@ class aa1Tasks(PSIDeviceBase): timestamp_ = 0 task_idx = int(self.taskIndex.get()) - def not_running(*args, old_value, value, timestamp, **kwargs): + def not_running(*args, value, timestamp, **kwargs): nonlocal timestamp_ - result = False if value[task_idx] in ["Running", 4] else True - # FIXME: BEC will swallow this exception - # error = bool(value[task_idx] in ["Error", 8]) + result = value[task_idx] not in ["Running", 4] timestamp_ = timestamp - # print(result) return result # Subscribe and wait for update diff --git a/tomcat_bec/devices/aerotech/__init__.py b/tomcat_bec/devices/aerotech/__init__.py index ad5fa52..477e0be 100644 --- a/tomcat_bec/devices/aerotech/__init__.py +++ b/tomcat_bec/devices/aerotech/__init__.py @@ -2,5 +2,3 @@ from .AerotechTasks import aa1Tasks from .AerotechPso import aa1AxisPsoDistance from .AerotechDriveDataCollection import aa1AxisDriveDataCollection from .AerotechAutomation1 import aa1Controller, aa1GlobalVariables, aa1GlobalVariableBindings, aa1AxisIo - - diff --git a/tomcat_bec/devices/gigafrost/gigafrostcamera.py b/tomcat_bec/devices/gigafrost/gigafrostcamera.py index b60c603..5d1181f 100644 --- a/tomcat_bec/devices/gigafrost/gigafrostcamera.py +++ b/tomcat_bec/devices/gigafrost/gigafrostcamera.py @@ -256,7 +256,7 @@ class GigaFrostCamera(PSIDetectorBase): # pylint: disable=too-many-instance-attributes custom_prepare_cls = GigaFrostCameraMixin - USER_ACCESS = [""] + USER_ACCESS = ["initialize"] _initialized = False infoBusyFlag = Component(EpicsSignalRO, "BUSY_STAT", auto_monitor=True) @@ -466,7 +466,7 @@ class GigaFrostCamera(PSIDetectorBase): def initialize(self): """ Initialization in separate command""" - self.custom_prepare_cls._init_gigafrost() + self.custom_prepare._init_gigafrost() self._initialized = True def trigger(self) -> DeviceStatus: diff --git a/tomcat_bec/devices/gigafrost/helgecamera.py b/tomcat_bec/devices/gigafrost/helgecamera.py new file mode 100644 index 0000000..f6067e0 --- /dev/null +++ b/tomcat_bec/devices/gigafrost/helgecamera.py @@ -0,0 +1,388 @@ +# -*- coding: utf-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 +import time +from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase + + +class HelgeCameraMixin(CustomDetectorMixin): + """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 + """ + + 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() + + + def arm_acquisition(self) -> None: + """Arm camera for acquisition""" + + # 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}") + + # 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: + """ + Monitor the detector status and send data. + """ + 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() + + + +class HelgeCameraCore(PSIDetectorBase): + """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 + 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 afull 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 + + """ + # 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) + + # ######################################################################## + # Acquisition commands + camStatusCmd = Component(EpicsSignal, "CAMERASTATUS", put_complete=True, kind=Kind.config) + + # ######################################################################## + # Acquisition configuration + acqExpTime = Component(EpicsSignalRO, "EXPOSURE", auto_monitor=True, kind=Kind.config) + + # ######################################################################## + # 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) + 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) + camInitBusy = Component(EpicsSignalRO, "BUSY_INIT", auto_monitor=True, kind=Kind.config) + + # ######################################################################## + # Throtled image preview + image = Component(EpicsSignalRO, "FPICTURE", kind=Kind.omitted) + + # ######################################################################## + # 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) + + @property + def state(self) -> str: + """ 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 "RUNNING" + #if self.camRemoval.value==0 and self.camInit.value==0: + if self.camInit.value==0: + return "OFFLINE" + #if self.camRemoval.value: + # return "REMOVED" + return "UNKNOWN" + + @state.setter + def state(self): + raise ReadOnlyError("State is a ReadOnly property") + + def configure(self, d: dict = {}) -> tuple: + 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() + + + def kickoff(self) -> DeviceStatus: + """ Start acquisition""" + return self.stage() + + def stop(self): + """ Stop the running acquisition """ + 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() + + + + + +class HelgeCameraBase(HelgeCameraCore): + """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. 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 + """ + + + + USER_ACCESS = ["describe", "shape", "bin", "roi"] + # ######################################################################## + # Additional status info + busy = Component(EpicsSignalRO, "BUSY", auto_monitor=True, kind=Kind.config) + camState = Component(EpicsSignalRO, "SS_CAMERA", auto_monitor=True, kind=Kind.config) + camProgress = Component(EpicsSignalRO, "CAMPROGRESS", auto_monitor=True, kind=Kind.config) + camRate = Component(EpicsSignalRO, "CAMRATE", auto_monitor=True, kind=Kind.config) + + # ######################################################################## + # 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) + + # ######################################################################## + # 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 + 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) + + # ######################################################################## + # 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: + """ + 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 + 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}") + # 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") + + + + + + + + + + + +# Automatically connect to test camera if directly invoked +if __name__ == "__main__": + + # Drive data collection + cam = HelgeCameraBase("SINBC02-DSRM310:", 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 432d0ab..3d9af26 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_client.py +++ b/tomcat_bec/devices/gigafrost/stddaq_client.py @@ -12,7 +12,9 @@ from threading import Thread import requests from ophyd import Device, Signal, Component, Kind, Staged -from websockets.sync.client import connect +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 @@ -34,81 +36,64 @@ class StdDaqMixin(CustomDeviceMixin): NOTE: Tomcat might use multiple cameras with their own separate DAQ instances. """ # 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}") + # NOTE: Scans don't have to fully configure the device d = {} - if 'kwargs' in scanparam: - scanargs = scanparam['kwargs'] + 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'] - # NOTE: Scans don't have to fully configure the device - points_total = 1 - if 'steps' in scanargs and scanargs['steps'] != None: - points_total *= scanargs['steps'] - if 'exp_burst' in scanargs and scanargs['exp_burst'] != None: - points_total *= scanargs['exp_burst'] - if 'repeats' in scanargs and scanargs['repeats']!= None: - points_total *= scanargs['repeats'] - if points_total != 1: - d['num_points_total'] = points_total + 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() + + if "daq_num_points" in scanargs: + d["num_points_total"] = scanargs["daq_num_points"] + else: + # Try to figure out number of points + num_points = 1 + points_valid = False + 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: + num_points *= scanargs["exp_burst"] + points_valid = True + if "repeats" in scanargs and scanargs['repeats'] is not None: + num_points *= scanargs["repeats"] + points_valid = True + if points_valid: + d["num_points_total"] = num_points # Perform bluesky-style configuration if len(d) > 0: - # Stop if current status is not idle - if self.parent.state() != "idle": - self.parent.surestop() # Configure new run (will restart the stdDAQ) - logger.warning(f"[{self.parent.name}] Configuring with:\n{d}") + logger.warning(f"[{self.parent.name}] stdDAQ needs reconfiguring with:\n{d}") self.parent.configure(d=d) + # Wait for REST API to kill the DAQ + sleep(0.5) - # Try to start a new run - file_path = self.parent.file_path.get() - num_images = self.parent.num_images.get() - message = {"command": "start", "path": file_path, "n_image": num_images} - ii = 0 - while True: - self.parent.connect() - reply = self.parent.message(message) - - if reply is not None: - reply = json.loads(reply) - self.parent.status.set(reply["status"], force=True).wait() - logger.info(f"[{self.parent.name}] Start DAQ reply: {reply}") - # Give it more time to reconfigure - if reply["status"] in ("rejected"): - # FIXME: running exposure is a nogo - if reply['reason'] == "gerhtrhjfjf": - raise RuntimeError(f"[{self.parent.name}] Start StdDAQ command rejected: already running") - else: - # Give it more time to restart - sleep(2) - else: - break - ii += 1 - if ii == 5: - break - if reply is not None and reply["status"] in ("rejected"): - raise RuntimeError( - f"Start StdDAQ command rejected (might be already running): {reply['reason']}" - ) + # Try to start a new run (reconnects) + self.parent.bluestage() # And start status monitoring - self._mon = Thread(target=self.poll, daemon=True) + self._mon = Thread(target=self.monitor, daemon=True) self._mon.start() def on_unstage(self): """ Stop a running acquisition and close connection """ - self.parent.surestop() + print("Creating virtual dataset") + self.parent.create_virtual_dataset() + self.parent.blueunstage() def on_stop(self): """ Stop a running acquisition and close connection """ - self.parent.surestop() + self.parent.blueunstage() - def poll(self) -> None: + def monitor(self) -> None: """ 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. @@ -117,8 +102,8 @@ class StdDaqMixin(CustomDeviceMixin): sleep(0.2) for msg in self.parent._wsclient: message = json.loads(msg) - self.parent.status.put(message["status"], force=True) - # logger.info(f"[{self.parent.name}] Pushed status: {message['status']}") + self.parent.runstatus.put(message["status"], force=True) + logger.info(f"[{self.parent.name}] Pushed status: {message['status']}") except (ConnectionClosedError, ConnectionClosedOK, AssertionError): # Libraty throws theese after connection is closed return @@ -132,11 +117,10 @@ class StdDaqMixin(CustomDeviceMixin): class StdDaqClient(PSIDeviceBase): """StdDaq API - This class combines the new websocket and REST interfaces of the stdDAQ replaced the documented - python client. The websocket interface starts and stops the acquisition and provides status, - while the REST interface can read and write the JSON configuration file. - - The DAQ needs to restart all services to reconfigure with a new config, which might corrupt + This class combines the new websocket and REST interfaces of the stdDAQ. The websocket + interface starts and stops the acquisition and provides status, while the REST interface + can read and write the JSON configuration file. The stdDAQ needs to restart all services + to reconfigure with a new config, which might corrupt the currently written files (fix is underway). Example: @@ -146,22 +130,22 @@ class StdDaqClient(PSIDeviceBase): """ # pylint: disable=too-many-instance-attributes custom_prepare_cls = StdDaqMixin - USER_ACCESS = ["set_daq_config", "get_daq_config", "surestop", "nuke", "connect", "message", "state"] + 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) - status = Component(Signal, value="unknown", kind=Kind.normal) + 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) # Configuration attributes - rest_url = Component(Signal, kind=Kind.config) + 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) cfg_pixel_height = Component(Signal, kind=Kind.config) cfg_pixel_width = Component(Signal, kind=Kind.config) - + cfg_nr_writers = Component(Signal, kind=Kind.config) def __init__( self, @@ -173,32 +157,23 @@ class StdDaqClient(PSIDeviceBase): configuration_attrs=None, parent=None, device_manager=None, - sim_mode=False, ws_url: str = "ws://localhost:8080", rest_url: str = "http://localhost:5000", + 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) - self.status._metadata["write_access"] = False - self.ws_url._metadata["write_access"] = False self.ws_url.set(ws_url, force=True).wait() - self.rest_url._metadata["write_access"] = False self.rest_url.set(rest_url, force=True).wait() + self.data_source_name = data_source_name - # Connect ro the DAQ and initialize values + # Connect to the DAQ and initialize values try: - self.read_daq_config() + self.get_daq_config(update=True) except Exception as ex: logger.error(f"Failed to connect to the stdDAQ REST API\n{ex}") - def __del__(self) -> None: - try: - self._wsclient.close() - except TypeError: - pass - return super().__del__() - - def connect(self): + def connect(self) -> ClientConnection: """Connect to the StdDAQ's websockets interface StdDAQ may reject connection for a few seconds after restart, or when @@ -207,16 +182,14 @@ class StdDaqClient(PSIDeviceBase): num_retry = 0 while num_retry < 5: try: - logger.debug(f"[{self.name}] Connecting to {self.ws_url.get()}") - self._wsclient = connect(self.ws_url.get()) - break + logger.debug(f"[{self.name}] Connecting to stdDAQ at {self.ws_url.get()}") + connection = connect(self.ws_url.get()) + logger.debug(f"[{self.name}] Connected to stdDAQ after {num_retry} tries") + return connection except ConnectionRefusedError: num_retry += 1 - sleep(3) - if num_retry == 5: - raise ConnectionRefusedError( - "The stdDAQ websocket interface refused connection 5 times.") - logger.debug(f"[{self.name}] Connected to DAQ after {num_retry} tries") + sleep(2) + raise ConnectionRefusedError("The stdDAQ websocket interface refused connection 5 times.") def message(self, message: dict, timeout=1, wait_reply=True, client=None) -> None | str: """Send a message to the StdDAQ and receive a reply @@ -224,9 +197,12 @@ class StdDaqClient(PSIDeviceBase): Note: finishing acquisition means StdDAQ will close connection, so there's no idle state polling. """ + # Prepare message + msg = json.dumps(message) if isinstance(message, dict) else str(message) + # Connect if client was destroyed if self._wsclient is None: - self.connect() + self._wsclient = self.connect() # Send message (reopen connection if needed) msg = json.dumps(message) if isinstance(message, dict) else str(message) @@ -234,7 +210,7 @@ class StdDaqClient(PSIDeviceBase): self._wsclient.send(msg) except (ConnectionClosedError, ConnectionClosedOK, AttributeError) as ex: # Re-connect if the connection was closed - self.connect() + self._wsclient = self.connect() self._wsclient.send(msg) # Wait for reply @@ -243,70 +219,167 @@ class StdDaqClient(PSIDeviceBase): try: reply = self._wsclient.recv(timeout) return reply - except (ConnectionClosedError, ConnectionClosedOK, TimeoutError, RuntimeError) as ex: + except (ConnectionClosedError, ConnectionClosedOK) as ex: + self._wsclient = None + logger.error(f"[{self.name}] WS connection was closed before reply: {ex}") + except (TimeoutError, RuntimeError) as ex: logger.error(f"[{self.name}] Error in receiving ws reply: {ex}") return reply def configure(self, d: dict = None): """Configure the next scan with the stdDAQ - Parameters as 'd' dictionary + Parameters as 'd' dictionary, the default is unchanged. ---------------------------- num_points_total : int, optional - Number of images to be taken during each scan. Set to -1 for an - unlimited number of images (limited by the ringbuffer size and - backend speed). (default = 10) - exposure : float, optional - Exposure time [ms]. (default = 0.2) - period : float, optional - Exposure period [ms], ignored in soft trigger mode. (default = 1.0) - pixel_width : int, optional - ROI size in the x-direction [pixels] (default = 2016) - pixel_height : int, optional - ROI size in the y-direction [pixels] (default = 2016) - scanid : int, optional - Scan identification number to be associated with the scan data - (default = 0) - trigger_mode : str, optional - Trigger mode of the gifafrost - (default = unchanged) - correction_mode : int, optional - The correction to be applied to the imaging data. The following - modes are available (default = 5): - - * 0: Bypass. No corrections are applied to the data. - * 1: Send correction factor A instead of pixel values - * 2: Send correction factor B instead of pixel values - * 3: Send correction factor C instead of pixel values - * 4: Invert pixel values, but do not apply any linearity correction - * 5: Apply the full linearity correction + Number of images to be taken during each scan. Set to -1 for an unlimited number of + images (limited by the ringbuffer size and backend speed). + file_path: str, optional + File path to save the data, usually GPFS. + image_width : int, optional + ROI size in the x-direction [pixels]. + image_height : int, optional + ROI size in the y-direction [pixels]. + bit_depth: int, optional + Image bit depth for cameras that can change it [int]. + nr_writers: int, optional + Number of writers [int]. """ - - if 'image_width' in d: + # 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: + 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 'file_path' in d: + 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() - if cfg['image_pixel_height'] != self.cfg_pixel_height.get() or cfg['image_pixel_width'] != self.cfg_pixel_width.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": - self.surestop() + raise RuntimeWarning(f"[{self.name}] stdDAQ reconfiguration might corrupt files") - # Stop running acquisition - self.unstage() # 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()) self.set_daq_config(cfg) - self.read_daq_config() + sleep(1) + self.get_daq_config(update=True) - def get_daq_config(self) -> dict: + def bluestage(self): + """ 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. + """ + # 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: + cam_img_w = self.device_manager.devices[self.data_source_name].cfgRoiX.get() + cam_img_h = self.device_manager.devices[self.data_source_name].cfgRoiY.get() + daq_img_w = self.cfg_pixel_width.get() + 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})") + else: + 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() + + # New connection + self._wsclient = self.connect() + message = {"command": "start", "path": file_path, "n_image": num_images, } + reply = self.message(message) + + if reply is not None: + reply = json.loads(reply) + self.runstatus.set(reply["status"], force=True).wait() + logger.info(f"[{self.name}] Start DAQ reply: {reply}") + + # 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") + else: + # Give it more time to consolidate + sleep(1) + else: + # Success!!! + 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']}") + + def blueunstage(self): + """ Unstages the stdDAQ + + Opens a new connection to the stdDAQ, sends the stop command and + waits for the idle state. + """ + ii = 0 + 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) + + # Let it consolidate + sleep(0.2) + + # Check final status (from new connection) + self._wsclient = self.connect() + reply = self.message({"command": "status"}) + 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}") + return + elif reply["status"] in ("stop"): + # Give it more time to stop + sleep(0.5) + elif ii >= 6: + raise RuntimeError(f"Failed to stop StdDAQ: {reply}") + ii += 1 + raise RuntimeError(f"Failed to stop StdDAQ in time") + + ########################################################################## + # 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 + + status = SubscriptionStatus(self.runstatus, is_running, settle_time=0.5) + return status + + def get_daq_config(self, update=False) -> dict: """Read the current configuration from the DAQ """ r = requests.get( @@ -315,20 +388,18 @@ class StdDaqClient(PSIDeviceBase): timeout=2) if r.status_code != 200: raise ConnectionError(f"[{self.name}] Error {r.status_code}:\t{r.text}") - return r.json() + cfg = r.json() - def read_daq_config(self) -> dict: - """Read the current configuration from the DAQ and update the ophyd device - """ - cfg = self.get_daq_config() - 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() + 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() return cfg - def set_daq_config(self, config): + def set_daq_config(self, config, settle_time=1): """Write a full configuration to the DAQ """ url = self.rest_url.get() + '/api/config/set' @@ -341,63 +412,50 @@ 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) + self.connect() return r.json() - def nuke(self): - """ Reconfigures the stdDAQ to restart the services + def create_virtual_dataset(self): + """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' + file_path = self.file_path.get() + + r = requests.post( + url, + params = {'user': 'ioc'}, + data = {'base_path': file_path, 'output_file': 'fede_virtual_test'}, + 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 + systemd to kill the current DAQ service and restart it with the same + configuration. Which might corrupt the currently written file... """ cfg = self.get_daq_config() self.set_daq_config(cfg) + sleep(restarttime) def state(self) -> str | None: - """ Querry the current system state""" - r = self.message({'command': 'status'}, wait_reply=True) - if r is None: - return None - else: + """ Querry the current system status""" + try: + wsclient = self.connect() + wsclient.send(json.dumps({'command': 'status'})) + r = wsclient.recv(timeout=1) r = json.loads(r) return r['status'] - - def surestop(self, timeout=5): - """ Stops a running acquisition - - REST reconfiguration restarts with systemd and can corrupt currently written files. - """ - # Retries to steal connection from poller - for rr in range(5): - client = connect(self.ws_url.get()) - msg = json.dumps({"command": "stop_all"}) - client.send(msg) - reply = client.recv(timeout=1) - reply = json.loads(reply) - # logger.warning(reply) - # if r['status'] == 'success': - # break - - sleep(0.5) - - client = connect(self.ws_url.get()) - msg = json.dumps({"command": "status"}) - client.send(msg) - reply = client.recv(timeout=1) - reply = json.loads(reply) - - if reply['status'] in ['idle', 'stoped']: - logger.warning(f"[{self.name}] Stop-all command finished in {reply['status']}") - return - - # If stop_all didn't stop, nuke the whole thing - logger.error(f"[{self.name}] Don't stop, make it rock!!!") - self.nuke() - sleep(timeout) - - - - - + except ConnectionRefusedError: + raise # Automatically connect to microXAS testbench if directly invoked if __name__ == "__main__": - daq = StdDaqClient(name="daq", ws_url="ws://xbl-daq-29:8080", rest_url="http://xbl-daq-29:5000") + daq = StdDaqClient(name="daq", ws_url="ws://sls-daq-001:8080", rest_url="http://sls-daq-001:5000") daq.wait_for_connection() diff --git a/tomcat_bec/devices/gigafrost/stddaq_preview.py b/tomcat_bec/devices/gigafrost/stddaq_preview.py index 8ee712d..3a08126 100644 --- a/tomcat_bec/devices/gigafrost/stddaq_preview.py +++ b/tomcat_bec/devices/gigafrost/stddaq_preview.py @@ -102,6 +102,7 @@ class StdDaqPreviewMixin(CustomDetectorMixin): 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( @@ -134,7 +135,7 @@ class StdDaqPreviewDetector(PSIDetectorBase): cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1') """ # Subscriptions for plotting image - USER_ACCESS = ["kickoff"] + USER_ACCESS = ["kickoff", "get_last_image"] SUB_MONITOR = "device_monitor_2d" _default_sub = SUB_MONITOR @@ -147,7 +148,8 @@ class StdDaqPreviewDetector(PSIDetectorBase): 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.config) + image = Component(Signal, kind=Kind.omitted) + _last_image = None def __init__( self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs @@ -180,6 +182,9 @@ class StdDaqPreviewDetector(PSIDetectorBase): sleep(1) self._socket.connect(self.url.get()) + def get_image(self): + return self._last_image + def kickoff(self) -> DeviceStatus: """ The DAQ was not meant to be toggled""" return DeviceStatus(self, done=True, success=True, settle_time=0.1) diff --git a/tomcat_bec/scans/tutorial_fly_scan.py b/tomcat_bec/scans/tutorial_fly_scan.py index 5846aa3..9b946a7 100644 --- a/tomcat_bec/scans/tutorial_fly_scan.py +++ b/tomcat_bec/scans/tutorial_fly_scan.py @@ -7,16 +7,30 @@ from bec_server.scan_server.scans import Acquire, AsyncFlyScanBase class AcquireDark(Acquire): scan_name = "acquire_dark" - required_kwargs = ["num"] - gui_config = {"Acquisition parameters": ["num"]} + required_kwargs = ["exp_burst"] + gui_config = {"Acquisition parameters": ["exp_burst"]} - def __init__(self, num: int, **kwargs): + def __init__(self, exp_burst: int, **kwargs): """ - Acquire a dark image. This scan is used to acquire a dark image. The dark image is an image taken with the shutter - closed and no beam on the sample. The dark image is used to correct the data images for dark current. + Acquire dark images. This scan is used to acquire dark images. Dark images are images taken with the shutter + closed and no beam on the sample. Dark images are used to correct the data images for dark current. Args: - num (int): number of dark images to acquire + + exp_burst : int + Number of dark images to acquire (no default) + 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] + image_width : int, optional + ROI size in the x-direction [pixels] + image_height : int, optional + ROI size in the y-direction [pixels] + acq_mode : str, optional + Predefined acquisition mode (default=) + file_path : str, optional + File path for standard daq Returns: ScanReport @@ -26,7 +40,7 @@ class AcquireDark(Acquire): """ super().__init__(**kwargs) - self.burst_at_each_point = num + self.burst_at_each_point = 1 # At each point, how many times I want to individually trigger self.scan_motors = ["eyex"] # change to the correct shutter device #self.shutter = "eyex" # change to the correct shutter device self.dark_shutter_pos = 0 ### change with a variable diff --git a/tomcat_bec/scripts/anotherroundsans.py b/tomcat_bec/scripts/anotherroundsans.py index f90fdea..d8d9247 100644 --- a/tomcat_bec/scripts/anotherroundsans.py +++ b/tomcat_bec/scripts/anotherroundsans.py @@ -14,8 +14,8 @@ def dev_disable_all(): """Disable all devices """ - for d in dev: - d.enabled = False + for k in dev: + dev[k].enabled = False @@ -41,12 +41,12 @@ 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") - - dev.es1_tasks.enabled = False - dev.es1_psod.enabled = False - dev.es1_ddaq.enabled = True + # if not bl_check_beam(): + # raise RuntimeError("Beamline is not in ready state") + + dev_disable_all() + dev.es1_roty.enabled = True + #dev.es1_ddaq.enabled = True dev.gfcam.enabled = True dev.gfdaq.enabled = True dev.daq_stream0.enabled = True diff --git a/tomcat_bec/scripts/scans_fede.py b/tomcat_bec/scripts/scans_fede.py index 373ab99..f1a09eb 100644 --- a/tomcat_bec/scripts/scans_fede.py +++ b/tomcat_bec/scripts/scans_fede.py @@ -1,61 +1,163 @@ -def fede_darks(nimages_dark, exposure_time=None, exposure_period=None, roix=None, roiy=None, acq_mode=None): +import os.path + +class Measurement: """ - Acquire a set of dark images with shutters closed. - - Parameters - ---------- - nimages_dark : int - 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 - 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=) - - - - Example: - -------- - tutorialdarks(num=100, exp_time=5) + This class provides a standard set of tomographic measurement functions + that can be used to acquire data at the TOMCAT beamline """ - 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 + def __init__(self): + self.sample_name = 'tmp' + self.data_path = 'disk_test' + self.nimages = 1000 + self.nimages_dark = 50 + self.nimages_white = 100 + + # To be able to keep what is already set on the camera + self.exposure_time = None + self.exposure_period = None + self.roix = None + self.roiy = None + + bec.system_config.file_suffix = self.sample_name + 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 + """ + 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) + + 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): + """ + Reconfigure the measurement with any number of new parameter + + Parameters + ---------- + 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) + data_path : string, optional + Information used to build the data directory for the measurement + (default=None) + exposure_time : float, optional + Exposure time [ms] (default=None) + exposure_period : float, optional + Exposure period [ms] (default=None) + roix : int, optional + ROI size in the x-direction [pixels] (default=None) + roiy : int, optional + ROI size in the y-direction [pixels] (default=None) + nimages : int, optional + Number of images to acquire (default=None) + nimages_dark : int, optional + Number of dark images to acquire (default=None) + nimages_white : int, optional + Number of white images to acquire (default=None) + """ + + if sample_name != None: + self.sample_name = sample_name + if data_path != None: + self.data_path = data_path + if nimages != None: + self.nimages = nimages + if nimages_dark != None: + self.nimages_dark = nimages_dark + if nimages_white != None: + self.nimages_white = nimages_white + 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() + + def acquire_darks(self,nimages_dark, exposure_time=None, exposure_period=None, + roix=None, roiy=None, acq_mode=None, file_path=None): + """ + Acquire a set of dark images with shutters closed. + + Parameters + ---------- + nimages_dark : int + 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 + 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) + file_path : str, optional + File path for standard daq (default=None) + + Example: + -------- + fede_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 - 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.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.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.cfgExpExt.set(0).wait() + dev.gfcam.cfgExpSoft.set(0).wait() + dev.gfcam.cfgExpTimer.set(1).wait() - dev.gfcam.cfgCntStartBit.set(0).wait() + dev.gfcam.cfgCntStartBit.set(0).wait() - # Commit changes to GF - dev.gfcam.cmdSetParam.set(1).wait() + # Commit changes to GF + dev.gfcam.cmdSetParam.set(1).wait() - ### TODO: camera reset - print("Handing over to 'scans.acquire_dark") - scans.acquire_dark(num=1, exp_burst=nimages_dark, exp_time=exposure_time, exp_period=exposure_period, image_width=roix, image_height=roiy) + 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 + if file_path!=None: + if os.path.isdir(file_path): + pass + else: + os.mkdir(file_path) + ### 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