From ed8d012632efd14ddb8a3a744db87ef277f459ac Mon Sep 17 00:00:00 2001 From: x12sa Date: Fri, 30 Jan 2026 12:55:06 +0100 Subject: [PATCH 01/12] feat: add logic for ext/en in ddg, mcs and flomni --- csaxs_bec/device_configs/bl_endstation.yaml | 12 ++-- csaxs_bec/device_configs/ptycho_flomni.yaml | 28 ++++---- .../epics/delay_generator_csaxs/README.md | 2 +- .../epics/delay_generator_csaxs/ddg_1.py | 30 +++++--- .../epics/delay_generator_csaxs/ddg_2.py | 6 +- csaxs_bec/devices/epics/fast_shutter.py | 72 +++++++++++++++++++ .../devices/epics/mcs_card/mcs_card_csaxs.py | 31 ++++++-- csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py | 2 + csaxs_bec/scans/flomni_fermat_scan.py | 11 ++- 9 files changed, 155 insertions(+), 39 deletions(-) create mode 100644 csaxs_bec/devices/epics/fast_shutter.py diff --git a/csaxs_bec/device_configs/bl_endstation.yaml b/csaxs_bec/device_configs/bl_endstation.yaml index 79f286d..5aa5025 100644 --- a/csaxs_bec/device_configs/bl_endstation.yaml +++ b/csaxs_bec/device_configs/bl_endstation.yaml @@ -425,7 +425,7 @@ sl5trxi: deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: C - host: x12sa-eb-smaract-mcs-02.psi.ch + host: x12sa-eb-smaract-mcs-05.psi.ch limits: - -200 - 200 @@ -447,7 +447,7 @@ sl5trxo: deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: D - host: x12sa-eb-smaract-mcs-02.psi.ch + host: x12sa-eb-smaract-mcs-05.psi.ch limits: - -200 - 200 @@ -469,7 +469,7 @@ sl5trxb: deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: E - host: x12sa-eb-smaract-mcs-02.psi.ch + host: x12sa-eb-smaract-mcs-05.psi.ch limits: - -200 - 200 @@ -491,7 +491,7 @@ sl5trxt: deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: F - host: x12sa-eb-smaract-mcs-02.psi.ch + host: x12sa-eb-smaract-mcs-05.psi.ch limits: - -200 - 200 @@ -513,7 +513,7 @@ xbimtrx: deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: A - host: x12sa-eb-smaract-mcs-02.psi.ch + host: x12sa-eb-smaract-mcs-05.psi.ch limits: - -200 - 200 @@ -535,7 +535,7 @@ xbimtry: deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor deviceConfig: axis_Id: B - host: x12sa-eb-smaract-mcs-02.psi.ch + host: x12sa-eb-smaract-mcs-05.psi.ch limits: - -200 - 200 diff --git a/csaxs_bec/device_configs/ptycho_flomni.yaml b/csaxs_bec/device_configs/ptycho_flomni.yaml index e2a20c5..db60cfd 100644 --- a/csaxs_bec/device_configs/ptycho_flomni.yaml +++ b/csaxs_bec/device_configs/ptycho_flomni.yaml @@ -408,20 +408,20 @@ cam_xeye: readOnly: false readoutPriority: async -cam_ids_rgb: - description: Camera flOMNI Xray eye ID203 - deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera - deviceConfig: - camera_id: 203 - bits_per_pixel: 24 - num_rotation_90: 3 - transpose: false - force_monochrome: true - m_n_colormode: 1 - enabled: true - onFailure: buffer - readOnly: false - readoutPriority: async +# cam_ids_rgb: +# description: Camera flOMNI Xray eye ID203 +# deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera +# deviceConfig: +# camera_id: 203 +# bits_per_pixel: 24 +# num_rotation_90: 3 +# transpose: false +# force_monochrome: true +# m_n_colormode: 1 +# enabled: true +# onFailure: buffer +# readOnly: false +# readoutPriority: async # ############################################################ diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/README.md b/csaxs_bec/devices/epics/delay_generator_csaxs/README.md index 439d920..acc9c4f 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/README.md +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/README.md @@ -37,7 +37,7 @@ to interrupt and ongoing sequence if needed. - a = t0 + 2ms (2ms delay to allow the shutter to open) - b = a + 1us (short pulse) - c = t0 -- d = a + exp_time * burst_count + 1ms (to allow the shutter to close) +- d = a + exp_time * burst_count - e = d - f = e + 1us (short pulse to OR gate for MCS triggering) diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py index 29a0623..5adf4b1 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py @@ -24,7 +24,7 @@ DELAY CHANNELS: - a = t0 + 2ms (2ms delay to allow the shutter to open) - b = a + 1us (short pulse) - c = t0 -- d = a + exp_time * burst_count + 1ms (to allow the shutter to close) +- d = a + exp_time * burst_count - e = d - f = e + 1us (short pulse to OR gate for MCS triggering) """ @@ -69,7 +69,7 @@ logger = bec_logger.logger # This can be adapted as needed, or fine-tuned per channel. On every reload of the # device configuration in BEC, these values will be set into the DDG1 device. _DEFAULT_CHANNEL_CONFIG: ChannelConfig = { - "amplitude": 5.0, + "amplitude": 4.5, "offset": 0.0, "polarity": OUTPUTPOLARITY.POSITIVE, "mode": "ttl", @@ -131,6 +131,8 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): device_manager (DeviceManagerBase | None, optional): Device manager. Defaults to None. """ + USER_ACCESS = ["keep_shutter_open_during_scan", "set_trigger"] + def __init__( self, name: str, @@ -142,6 +144,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): super().__init__( name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs ) + self._shutter_to_open_delay = 2e-3 self.device_manager = device_manager self._poll_thread = threading.Thread(target=self._poll_event_status, daemon=True) self._poll_thread_run_event = threading.Event() @@ -192,6 +195,12 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): self.burst_delay.put(0) self.burst_count.put(1) + def keep_shutter_open_during_scan(self, open:True) -> None: + if open is True: + self._shutter_to_open_delay = 0 + else: + self._shutter_to_open_delay = 2e-3 + def on_stage(self) -> None: """ @@ -239,27 +248,30 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # Burst Period DDG1 # Set burst_period to shutter width - # c/t0 + 2ms + exp_time * burst_count + 1ms - shutter_width = 2e-3 + exp_time * frames_per_trigger + 1e-3 + # c/t0 + self._shutter_to_open_delay + exp_time * burst_count + shutter_width = self._shutter_to_open_delay + exp_time * frames_per_trigger # Shutter starts closing at end of exposure if self.burst_period.get() != shutter_width: self.burst_period.put(shutter_width) # Trigger DDG2 # a = t0 + 2ms, b = a + 1us # a has reference to t0, b has reference to a - # Add delay of 2ms to allow shutter to open - self.set_delay_pairs(channel="ab", delay=2e-3, width=1e-6) + # Add delay of self._shutter_to_open_delay to allow shutter to open + self.set_delay_pairs(channel="ab", delay=self._shutter_to_open_delay, width=1e-6) # Trigger shutter - # d = c/t0 + 2ms + exp_time * burst_count + 1ms + # d = c/t0 + self._shutter_to_open_delay + exp_time * burst_count + 1ms # c has reference to t0, d has reference to c - # Shutter opens without delay at t0, closes after exp_time * burst_count + 3ms (2ms open, 1ms close) + # Shutter opens without delay at t0, closes after exp_time * burst_count + 2ms (self._shutter_to_open_delay) self.set_delay_pairs(channel="cd", delay=0, width=shutter_width) # Trigger extra pulse for MCS OR gate # f = e + 1us # e has refernce to d, f has reference to e - self.set_delay_pairs(channel="ef", delay=0, width=1e-6) + if self.scan_info.msg.scan_type == "fly": + self.set_delay_pairs(channel="ef", delay=0, width=0) + else: + self.set_delay_pairs(channel="ef", delay=0, width=1e-6) # NOTE Add additional sleep to make sure that the IOC and DDG HW process the values properly # This value has been choosen empirically after testing with the HW. It's diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py index bf781f3..e9ee1da 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py @@ -37,6 +37,7 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import ChannelConfig, DelayGeneratorCSAXS, LiteralChannels, + BURSTCONFIG, ) logger = bec_logger.logger @@ -47,7 +48,7 @@ logger = bec_logger.logger # NOTE Default channel configuration for the DDG2 delay generator channels _DEFAULT_CHANNEL_CONFIG: ChannelConfig = { - "amplitude": 5.0, + "amplitude": 4.5, "offset": 0.0, "polarity": OUTPUTPOLARITY.POSITIVE, "mode": "ttl", @@ -134,6 +135,9 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS): self.set_trigger(DEFAULT_TRIGGER_SOURCE) self.set_references_for_channels(DEFAULT_REFERENCES) + # Set burst config + self.burst_config.put(BURSTCONFIG.FIRST_CYCLE.value) + def on_stage(self) -> DeviceStatus | StatusBase | None: """ diff --git a/csaxs_bec/devices/epics/fast_shutter.py b/csaxs_bec/devices/epics/fast_shutter.py new file mode 100644 index 0000000..2ae4ce1 --- /dev/null +++ b/csaxs_bec/devices/epics/fast_shutter.py @@ -0,0 +1,72 @@ +import time +import socket +from ophyd import Component as Cpt +from ophyd import Device, Kind +from ophyd import EpicsSignal + + +class FastEpicsShutterError(Exception): + pass + + +class cSAXSFastEpicsShutter(Device): + """ + Fast EPICS shutter with automatic PV selection based on host subnet. IOC prefix is 'X12SA-ES1-TTL:' + """ + + USER_ACCESS = ["fshopen", "fshclose", "fshstatus", "fshinfo", "fshstatus_readback", "help"] + SUB_VALUE = "value" + _default_sub = SUB_VALUE + + # PVs + shutter = Cpt(EpicsSignal, "OUT_01", kind=Kind.normal, auto_monitor=True) + shutter_readback = Cpt(EpicsSignal, "IN_01", kind=Kind.normal, auto_monitor=True) + + + # ----------------------------------------------------- + # User-facing shutter control functions + # ----------------------------------------------------- + + def fshopen(self): + """Open the fast shutter.""" + try: + self.shutter.put(1, wait=True) + except Exception as ex: + raise OMNYFastEpicsShutterError(f"Failed to open shutter: {ex}") + + def fshclose(self): + """Close the fast shutter.""" + try: + self.shutter.put(0, wait=True) + except Exception as ex: + raise OMNYFastEpicsShutterError(f"Failed to close shutter: {ex}") + + def fshstatus(self): + """Return the fast shutter status (0=closed, 1=open).""" + try: + return self.shutter.get() + except Exception as ex: + raise OMNYFastEpicsShutterError(f"Failed to read shutter status: {ex}") + + def fshstatus_readback(self): + """Return the fast shutter status (0=closed, 1=open).""" + try: + return self.shutter_readback.get() + except Exception as ex: + raise OMNYFastEpicsShutterError(f"Failed to read shutter status: {ex}") + + def fshinfo(self): + """Print information about which EPICS PV channel is being used.""" + pvname = self.shutter.pvname + shutter_readback_pvname = self.shutter_readback.pvname + print(f"Fast shutter connected to EPICS channel: {pvname} with shutter readback: {shutter_readback_pvname}") + return pvname + + def help(self): + """Display available user methods.""" + print("Available methods:") + for method in self.USER_ACCESS: + print(f" - {method}") + +if __name__ == "__main__": + fsh = cSAXSFastEpicsShutter(name="fsh", prefix="X12SA-ES1-TTL:") diff --git a/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py b/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py index fdda685..f4a7b7c 100644 --- a/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py +++ b/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py @@ -22,7 +22,7 @@ import numpy as np from bec_lib.logger import bec_logger from ophyd import Component as Cpt from ophyd import EpicsSignalRO, Kind -from ophyd_devices import AsyncMultiSignal, CompareStatus, ProgressSignal, StatusBase +from ophyd_devices import AsyncMultiSignal, CompareStatus, ProgressSignal, StatusBase, TransitionStatus from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase from csaxs_bec.devices.epics.mcs_card.mcs_card import ( @@ -255,6 +255,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): **kwargs: Additional keyword arguments from the subscription, including 'obj' (the EpicsSignalRO instance). """ with self._rlock: + logger.info(f"Received update on mcs card {self.name}") if self._omit_mca_callbacks.is_set(): return # Suppress callbacks when erasing all channels self._mca_counter_index += 1 @@ -286,7 +287,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): ) # Once we have received all channels, push data to BEC and reset for next accumulation - logger.debug( + logger.info( f"Received update for {attr_name}, index {self._mca_counter_index}/{self.NUM_MCA_CHANNELS}" ) if len(self._current_data) == self.NUM_MCA_CHANNELS: @@ -363,7 +364,11 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): self._num_total_triggers = triggers * num_points self._acquisition_group = "monitored" if triggers == 1 else "burst_group" self.preset_real.set(0).wait(timeout=self._pv_timeout) - self.num_use_all.set(triggers).wait(timeout=self._pv_timeout) + if self.scan_info.msg.scan_type == "step": + self.num_use_all.set(triggers).wait(timeout=self._pv_timeout) + elif self.scan_info.msg.scan_type == "fly": + self.num_use_all.set(self._num_total_triggers).wait(timeout=self._pv_timeout) + # Clear any previous data, just to be sure with self._rlock: @@ -385,6 +390,12 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): self._omit_mca_callbacks.clear() logger.info(f"MCS Card {self.name} on_stage completed in {time.time() - start_time:.3f}s.") + # For a fly scan we need to start the mcs card ourselves + if self.scan_info.msg.scan_type == "fly": + status_acquiring = TransitionStatus(self.acquiring, [ACQUIRING.DONE, ACQUIRING.ACQUIRING]) + self.cancel_on_stop(status_acquiring) + self.erase_start.put(1) + def on_unstage(self) -> None: """ @@ -422,9 +433,16 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): hasattr(self.scan_info.msg, "num_points") and self.scan_info.msg.num_points is not None ): - if self._current_data_index == self.scan_info.msg.num_points: - for callback in self._scan_done_callbacks: - callback(exception=None) + if self.scan_info.msg.scan_type == "step": + if self._current_data_index == self.scan_info.msg.num_points: + for callback in self._scan_done_callbacks: + callback(exception=None) + else: + logger.info(f"Current data index is {self._current_data_index}") + if self._current_data_index >=1: + for callback in self._scan_done_callbacks: + callback(exception=None) + time.sleep(0.02) # 20ms delay to avoid busy loop except Exception as exc: # pylint: disable=broad-except content = traceback.format_exc() @@ -478,6 +496,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): monitoring thread is stopped properly. """ + self.software_channel_advance.put(1) # Prepare and register status callback for the async monitoring loop status_async_data = StatusBase(obj=self) self._scan_done_callbacks.append(partial(self._status_callback, status_async_data)) diff --git a/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py b/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py index a4eaee4..92e3754 100644 --- a/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py +++ b/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py @@ -629,6 +629,8 @@ class RtFlomniMotor(Device, PositionerBase): SUB_CONNECTION_CHANGE = "connection_change" _default_sub = SUB_READBACK + connectionTimeout = 20 + def __init__( self, axis_Id, diff --git a/csaxs_bec/scans/flomni_fermat_scan.py b/csaxs_bec/scans/flomni_fermat_scan.py index 045ef1e..7402358 100644 --- a/csaxs_bec/scans/flomni_fermat_scan.py +++ b/csaxs_bec/scans/flomni_fermat_scan.py @@ -27,6 +27,7 @@ from bec_lib import bec_logger, messages from bec_lib.endpoints import MessageEndpoints from bec_server.scan_server.errors import ScanAbortion from bec_server.scan_server.scans import SyncFlyScanBase +from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import TRIGGERSOURCE logger = bec_logger.logger @@ -73,14 +74,13 @@ class FlomniFermatScan(SyncFlyScanBase): >>> scans.flomni_fermat_scan(fovx=20, fovy=25, cenx=0.02, ceny=0, zshift=0, angle=0, step=0.5, exp_time=0.01) """ - super().__init__(parameter=parameter, **kwargs) + super().__init__(parameter=parameter, exp_time=exp_time, **kwargs) self.show_live_table = False self.axis = [] self.fovx = fovx self.fovy = fovy self.cenx = cenx self.ceny = ceny - self.exp_time = exp_time self.step = step self.zshift = zshift self.angle = angle @@ -151,6 +151,9 @@ class FlomniFermatScan(SyncFlyScanBase): yield from self.stubs.send_rpc_and_wait("rty", "set", self.positions[0][1]) def _prepare_setup_part2(self): + # Prepare DDG1 to use + yield from self.stubs.send_rpc_and_wait("ddg1", "set_trigger", TRIGGERSOURCE.EXT_RISING_EDGE.value) + if self.flomni_rotation_status: self.flomni_rotation_status.wait() @@ -307,6 +310,10 @@ class FlomniFermatScan(SyncFlyScanBase): logger.warning("No positions found to return to start") + def cleanup(self): + yield from self.stubs.send_rpc_and_wait("ddg1", "set_trigger", TRIGGERSOURCE.SINGLE_SHOT.value) + yield from super().cleanup() + def run(self): self.initialize() yield from self.read_scan_motors() -- 2.49.1 From 6fad4f20340f69085b0e4daa9f8dc275bb6806b5 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 9 Feb 2026 08:29:08 +0100 Subject: [PATCH 02/12] fix(fast-shutter): Fix cSAXS fast shutter device --- csaxs_bec/devices/epics/fast_shutter.py | 58 +++++------- csaxs_bec/devices/omny/shutter.py | 105 +++++++++++----------- tests/tests_devices/test_epics_devices.py | 37 ++++++++ 3 files changed, 116 insertions(+), 84 deletions(-) create mode 100644 tests/tests_devices/test_epics_devices.py diff --git a/csaxs_bec/devices/epics/fast_shutter.py b/csaxs_bec/devices/epics/fast_shutter.py index 2ae4ce1..9064b15 100644 --- a/csaxs_bec/devices/epics/fast_shutter.py +++ b/csaxs_bec/devices/epics/fast_shutter.py @@ -1,12 +1,11 @@ -import time -import socket +""" +Shutter device for the cSAXS beamline with 2 PVs. One is connected to a +signal can be set to control the shutter signal, and the other is a readback signal +that can be monitored to check the shutter status as it may be controlled directly by +the delay generator.""" + from ophyd import Component as Cpt -from ophyd import Device, Kind -from ophyd import EpicsSignal - - -class FastEpicsShutterError(Exception): - pass +from ophyd import Device, EpicsSignal, Kind class cSAXSFastEpicsShutter(Device): @@ -22,45 +21,35 @@ class cSAXSFastEpicsShutter(Device): shutter = Cpt(EpicsSignal, "OUT_01", kind=Kind.normal, auto_monitor=True) shutter_readback = Cpt(EpicsSignal, "IN_01", kind=Kind.normal, auto_monitor=True) - # ----------------------------------------------------- # User-facing shutter control functions # ----------------------------------------------------- - def fshopen(self): + # pylint: disable=protetced-access + def fshopen(self) -> None: """Open the fast shutter.""" - try: - self.shutter.put(1, wait=True) - except Exception as ex: - raise OMNYFastEpicsShutterError(f"Failed to open shutter: {ex}") + self.shutter.set(1).wait(timeout=self.shutter._timeout) # 2s default for ES - def fshclose(self): + # pylint: disable=protetced-access + def fshclose(self) -> None: """Close the fast shutter.""" - try: - self.shutter.put(0, wait=True) - except Exception as ex: - raise OMNYFastEpicsShutterError(f"Failed to close shutter: {ex}") + self.shutter.set(0).wait(timeout=self.shutter._timeout) # 2s default for ES - def fshstatus(self): - """Return the fast shutter status (0=closed, 1=open).""" - try: - return self.shutter.get() - except Exception as ex: - raise OMNYFastEpicsShutterError(f"Failed to read shutter status: {ex}") + def fshstatus(self) -> int: + """Return the fast shutter control status (0=closed, 1=open).""" + return self.shutter.get() # Ensure we have the latest value from EPICS - def fshstatus_readback(self): + def fshstatus_readback(self) -> int: """Return the fast shutter status (0=closed, 1=open).""" - try: - return self.shutter_readback.get() - except Exception as ex: - raise OMNYFastEpicsShutterError(f"Failed to read shutter status: {ex}") - - def fshinfo(self): + return self.shutter_readback.get() # Ensure we have the latest value from EPICS + + def fshinfo(self) -> None: """Print information about which EPICS PV channel is being used.""" pvname = self.shutter.pvname shutter_readback_pvname = self.shutter_readback.pvname - print(f"Fast shutter connected to EPICS channel: {pvname} with shutter readback: {shutter_readback_pvname}") - return pvname + print( + f"Fast shutter connected to EPICS channel: {pvname} with shutter readback: {shutter_readback_pvname}" + ) def help(self): """Display available user methods.""" @@ -68,5 +57,6 @@ class cSAXSFastEpicsShutter(Device): for method in self.USER_ACCESS: print(f" - {method}") + if __name__ == "__main__": fsh = cSAXSFastEpicsShutter(name="fsh", prefix="X12SA-ES1-TTL:") diff --git a/csaxs_bec/devices/omny/shutter.py b/csaxs_bec/devices/omny/shutter.py index 3ffd7e6..983b632 100644 --- a/csaxs_bec/devices/omny/shutter.py +++ b/csaxs_bec/devices/omny/shutter.py @@ -1,82 +1,87 @@ -import time -import socket +""" +Fast Shutter control for OMNY setup. If started with a config file in which the device_manager +has a 'fsh' device (cSAXSFastEpicsShutter), this device will be used as the shutter. +Otherwise, the device will create a dummy shutter device that will log warnings when shutter +methods are called, but will not raise exceptions. +""" + +from bec_lib.logger import bec_logger from ophyd import Component as Cpt -from ophyd import Device -from ophyd import EpicsSignal +from ophyd import Device, Signal +from ophyd_devices import PSIDeviceBase + +logger = bec_logger.logger -class OMNYFastEpicsShutterError(Exception): - pass - - -def _detect_host_pv(): - """Detect host subnet and return appropriate PV name.""" - try: - hostname = socket.gethostname() - local_ip = socket.gethostbyname(hostname) - if local_ip.startswith("129.129.122."): - return "X12SA-ES1-TTL:OUT_01" - else: - return "XOMNYI-XEYE-DUMMYSHUTTER:0" - except Exception as ex: - print(f"Warning: could not detect IP subnet ({ex}), using dummy shutter.") - return "XOMNYI-XEYE-DUMMYSHUTTER:0" - - -class OMNYFastEpicsShutter(Device): +class OMNYFastEpicsShutter(PSIDeviceBase, Device): """ - Fast EPICS shutter with automatic PV selection based on host subnet. + Fast Shutter control for OMNY setup. If started with at the beamline, it will expose + the shutter control methods (fshopen, fshclose, fshstatus, fshinfo) from the + cSAXSFastEpicsShutter device. The device is identified by the 'fsh' name in the device manager. + If the 'fsh' device is not found in the device manager, this device will create a dummy shutter + and log warnings when shutter methods are called, but will not raise exceptions. """ - USER_ACCESS = ["fshopen", "fshclose", "fshstatus", "fshinfo", "help"] + USER_ACCESS = ["fshopen", "fshclose", "fshstatus", "fshinfo", "help", "fshstatus_readback"] SUB_VALUE = "value" _default_sub = SUB_VALUE - # PV is detected dynamically at import time - shutter = Cpt(EpicsSignal, name="shutter", read_pv=_detect_host_pv(), auto_monitor=True) - - def __init__(self, prefix="", *, name, **kwargs): - super().__init__(prefix, name=name, **kwargs) - self.shutter.subscribe(self._emit_value) - - def _emit_value(self, **kwargs): - timestamp = kwargs.pop("timestamp", time.time()) - self.wait_for_connection() - self._run_subs(sub_type=self.SUB_VALUE, timestamp=timestamp, obj=self) + shutter = Cpt(Signal, name="shutter") # ----------------------------------------------------- # User-facing shutter control functions # ----------------------------------------------------- + # pylint: disable=invalid-name + def _check_if_cSAXS_shutter_exists_in_config(self) -> bool: + """ + Check on the device manager if the shutter device exists. + + Returns: + bool: True if the 'fsh' device exists in the device manager, False otherwise + """ + if self.device_manager.devices.get("fsh", None) is None: + logger.warning(f"Fast shutter device not found for {self.name}.") + return False + return True + def fshopen(self): """Open the fast shutter.""" - try: - self.shutter.put(1, wait=True) - except Exception as ex: - raise OMNYFastEpicsShutterError(f"Failed to open shutter: {ex}") + if self._check_if_cSAXS_shutter_exists_in_config(): + return self.device_manager.devices["fsh"].fshopen() + else: + self.shutter.put(1) def fshclose(self): """Close the fast shutter.""" - try: - self.shutter.put(0, wait=True) - except Exception as ex: - raise OMNYFastEpicsShutterError(f"Failed to close shutter: {ex}") + if self._check_if_cSAXS_shutter_exists_in_config(): + return self.device_manager.devices["fsh"].fshclose() + else: + self.shutter.put(0) def fshstatus(self): """Return the fast shutter status (0=closed, 1=open).""" - try: + if self._check_if_cSAXS_shutter_exists_in_config(): + return self.device_manager.devices["fsh"].fshstatus() + else: return self.shutter.get() - except Exception as ex: - raise OMNYFastEpicsShutterError(f"Failed to read shutter status: {ex}") def fshinfo(self): """Print information about which EPICS PV channel is being used.""" - pvname = self.shutter.pvname - print(f"Fast shutter connected to EPICS channel: {pvname}") - return pvname + if self._check_if_cSAXS_shutter_exists_in_config(): + return self.device_manager.devices["fsh"].fshinfo() + else: + print("Using dummy fast shutter device. No EPICS channel is connected.") def help(self): """Display available user methods.""" print("Available methods:") for method in self.USER_ACCESS: print(f" - {method}") + + def fshstatus_readback(self): + """Return the fast shutter status (0=closed, 1=open) from the readback signal.""" + if self._check_if_cSAXS_shutter_exists_in_config(): + return self.device_manager.devices["fsh"].fshstatus_readback() + else: + self.shutter.get() diff --git a/tests/tests_devices/test_epics_devices.py b/tests/tests_devices/test_epics_devices.py new file mode 100644 index 0000000..327847b --- /dev/null +++ b/tests/tests_devices/test_epics_devices.py @@ -0,0 +1,37 @@ +"""Module to test epics devices.""" + +import pytest +from ophyd_devices.tests.utils import patched_device + +from csaxs_bec.devices.epics.fast_shutter import cSAXSFastEpicsShutter + + +@pytest.fixture +def fast_shutter_device(): + """Fixture to create a patched cSAXSFastEpicsShutter device for testing.""" + with patched_device(cSAXSFastEpicsShutter, name="fsh", prefix="X12SA-ES1-TTL:") as device: + yield device + + +def test_fast_shutter_methods(fast_shutter_device): + """Test the user-facing methods of the cSAXSFastEpicsShutter device.""" + assert fast_shutter_device.name == "fsh", "Device name should be 'fsh'" + assert fast_shutter_device.prefix == "X12SA-ES1-TTL:", "Device prefix is 'X12SA-ES1-TTL:'" + # Test fshopen and fshclose + fast_shutter_device.fshopen() + assert fast_shutter_device.shutter.get() == 1, "Shutter should be open (1) after fshopen()" + assert fast_shutter_device.fshstatus() == 1, "fshstatus should return 1 when shutter is open" + + fast_shutter_device.fshclose() + assert fast_shutter_device.shutter.get() == 0, "Shutter should be closed (0) after fshclose()" + assert fast_shutter_device.fshstatus() == 0, "fshstatus should return 0 when shutter is closed" + + # shutter_readback is connected to separate PV. + fast_shutter_device.shutter_readback._read_pv.mock_data = 1 # Simulate readback showing open + assert ( + fast_shutter_device.fshstatus_readback() == 1 + ), "fshstatus_readback should return 1 when shutter readback shows open" + fast_shutter_device.shutter_readback._read_pv.mock_data = 0 # Simulate readback showing closed + assert ( + fast_shutter_device.fshstatus_readback() == 0 + ), "fshstatus_readback should return 0 when shutter readback shows closed" -- 2.49.1 From c3aa882b1d984fef3e4d856e3c82aae9b3fa3b9a Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 9 Feb 2026 08:39:19 +0100 Subject: [PATCH 03/12] fix(configs): add connectionTimeout=20s defaults to devices with SocketController --- csaxs_bec/device_configs/bl_endstation.yaml | 23 ++++++++++++++++ csaxs_bec/device_configs/bl_optics_hutch.yaml | 4 +++ csaxs_bec/device_configs/ptycho_flomni.yaml | 23 +++++++++++++++- csaxs_bec/device_configs/ptycho_lamni.yaml | 13 +++++++++ csaxs_bec/device_configs/ptycho_omny.yaml | 27 +++++++++++++++++++ csaxs_bec/device_configs/user_template.yaml | 2 ++ 6 files changed, 91 insertions(+), 1 deletion(-) diff --git a/csaxs_bec/device_configs/bl_endstation.yaml b/csaxs_bec/device_configs/bl_endstation.yaml index 5aa5025..3347e25 100644 --- a/csaxs_bec/device_configs/bl_endstation.yaml +++ b/csaxs_bec/device_configs/bl_endstation.yaml @@ -60,6 +60,7 @@ xbpm3x: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: -22.5 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -82,6 +83,7 @@ xbpm3y: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: -2 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -104,6 +106,7 @@ sl3trxi: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: -5.5 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -126,6 +129,7 @@ sl3trxo: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: 6 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -148,6 +152,7 @@ sl3trxb: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: -5.8 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -170,6 +175,7 @@ sl3trxt: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: 5.5 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -192,6 +198,7 @@ fast_shutter_n1_x: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: -7 in: 0 @@ -215,6 +222,7 @@ fast_shutter_o1_x: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: -15.8 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -237,6 +245,7 @@ fast_shutter_o2_x: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: -15.5 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -259,6 +268,7 @@ filter_array_1_x: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: 25 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -281,6 +291,7 @@ filter_array_2_x: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: 25.5 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -303,6 +314,7 @@ filter_array_3_x: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: 25.8 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -325,6 +337,7 @@ filter_array_4_x: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: 25 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -347,6 +360,7 @@ sl4trxi: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: -5.5 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -369,6 +383,7 @@ sl4trxo: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: 6 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -391,6 +406,7 @@ sl4trxb: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: -5.8 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -413,6 +429,7 @@ sl4trxt: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: 5.5 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -437,6 +454,7 @@ sl5trxi: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: -6 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -459,6 +477,7 @@ sl5trxo: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: 5.5 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -481,6 +500,7 @@ sl5trxb: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: -5.5 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -503,6 +523,7 @@ sl5trxt: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: 6 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -525,6 +546,7 @@ xbimtrx: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: -14.7 # bl_smar_stage to use csaxs reference method. assign number according to axis channel @@ -547,6 +569,7 @@ xbimtry: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: init_position: 0 # bl_smar_stage to use csaxs reference method. assign number according to axis channel diff --git a/csaxs_bec/device_configs/bl_optics_hutch.yaml b/csaxs_bec/device_configs/bl_optics_hutch.yaml index 7ce2815..5dcf9f9 100644 --- a/csaxs_bec/device_configs/bl_optics_hutch.yaml +++ b/csaxs_bec/device_configs/bl_optics_hutch.yaml @@ -89,6 +89,7 @@ xbpm2x: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: # bl_smar_stage to use csaxs reference method. assign number according to axis channel bl_smar_stage: 0 @@ -108,6 +109,7 @@ xbpm2y: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: # bl_smar_stage to use csaxs reference method. assign number according to axis channel bl_smar_stage: 1 @@ -127,6 +129,7 @@ cu_foilx: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: # bl_smar_stage to use csaxs reference method. assign number according to axis channel bl_smar_stage: 2 @@ -146,6 +149,7 @@ scinx: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: # bl_smar_stage to use csaxs reference method. assign number according to axis channel bl_smar_stage: 3 diff --git a/csaxs_bec/device_configs/ptycho_flomni.yaml b/csaxs_bec/device_configs/ptycho_flomni.yaml index db60cfd..7ca30d6 100644 --- a/csaxs_bec/device_configs/ptycho_flomni.yaml +++ b/csaxs_bec/device_configs/ptycho_flomni.yaml @@ -17,6 +17,7 @@ feyex: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: -16.267 out: -1 @@ -35,6 +36,7 @@ feyey: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: -10.467 fheater: @@ -52,6 +54,7 @@ fheater: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 foptx: description: Optics X deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor @@ -67,6 +70,7 @@ foptx: onFailure: buffer readOnly: true readoutPriority: baseline + connectionTimeout: 20 userParameter: in: -13.761 fopty: @@ -84,6 +88,7 @@ fopty: onFailure: buffer readOnly: true readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 0.552 out: 0.752 @@ -102,6 +107,7 @@ foptz: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 23 fsamroy: @@ -119,6 +125,7 @@ fsamroy: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 fsamx: description: Sample coarse X deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor @@ -134,6 +141,7 @@ fsamx: onFailure: buffer readOnly: true readoutPriority: baseline + connectionTimeout: 20 userParameter: in: -1.1 fsamy: @@ -150,7 +158,8 @@ fsamy: enabled: true onFailure: buffer readOnly: true - readoutPriority: baseline + readoutPriority: baseline' + connectionTimeout: 20 userParameter: in: 2.75 ftracky: @@ -168,6 +177,7 @@ ftracky: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 ftrackz: description: Laser Tracker coarse Z deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor @@ -183,6 +193,7 @@ ftrackz: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 ftransx: description: Sample transer X deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor @@ -198,6 +209,7 @@ ftransx: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 ftransy: description: Sample transer Y deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor @@ -213,6 +225,7 @@ ftransy: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: sensor_voltage: -2.4 ftransz: @@ -230,6 +243,7 @@ ftransz: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 ftray: description: Sample transfer tray deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor @@ -245,6 +259,7 @@ ftray: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 ############################################################ @@ -279,6 +294,7 @@ fosax: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 9.124 out: 5.3 @@ -297,6 +313,7 @@ fosay: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 0.367 fosaz: @@ -314,6 +331,7 @@ fosaz: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 8.5 out: 6 @@ -334,6 +352,7 @@ rtx: onFailure: buffer readOnly: false readoutPriority: on_request + connectionTimeout: 20 userParameter: low_signal: 10000 min_signal: 9000 @@ -350,6 +369,7 @@ rty: onFailure: buffer readOnly: false readoutPriority: on_request + connectionTimeout: 20 userParameter: tomo_additional_offsety: 0 rtz: @@ -364,6 +384,7 @@ rtz: onFailure: buffer readOnly: false readoutPriority: on_request + connectionTimeout: 20 ############################################################ ####################### Cameras ############################ diff --git a/csaxs_bec/device_configs/ptycho_lamni.yaml b/csaxs_bec/device_configs/ptycho_lamni.yaml index cb9bcd1..cd3c250 100644 --- a/csaxs_bec/device_configs/ptycho_lamni.yaml +++ b/csaxs_bec/device_configs/ptycho_lamni.yaml @@ -19,6 +19,7 @@ leyex: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 14.117 leyey: @@ -38,6 +39,7 @@ leyey: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 48.069 out: 0.5 @@ -58,6 +60,7 @@ loptx: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: -0.244 out: -0.699 @@ -78,6 +81,7 @@ lopty: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 3.724 out: 3.53 @@ -98,6 +102,7 @@ loptz: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 lsamrot: description: Sample rotation deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor @@ -115,6 +120,7 @@ lsamrot: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 lsamx: description: Sample coarse X deviceClass: csaxs_bec.devices.omny.galil.lgalil_ophyd.LamniGalilMotor @@ -132,6 +138,7 @@ lsamx: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: center: 8.768 lsamy: @@ -151,6 +158,7 @@ lsamy: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: center: 10.041 @@ -176,6 +184,7 @@ losax: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: -1.442 losay: @@ -195,6 +204,7 @@ losay: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: -0.171 out: 3.8 @@ -215,6 +225,7 @@ losaz: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: -1 out: -3 @@ -238,6 +249,7 @@ rtx: deviceTags: - lamni readoutPriority: baseline + connectionTimeout: 20 enabled: true readOnly: False rty: @@ -255,6 +267,7 @@ rty: deviceTags: - lamni readoutPriority: baseline + connectionTimeout: 20 enabled: true readOnly: False diff --git a/csaxs_bec/device_configs/ptycho_omny.yaml b/csaxs_bec/device_configs/ptycho_omny.yaml index edce1a7..cd894a1 100755 --- a/csaxs_bec/device_configs/ptycho_omny.yaml +++ b/csaxs_bec/device_configs/ptycho_omny.yaml @@ -69,6 +69,7 @@ rtx: onFailure: buffer readOnly: false readoutPriority: on_request + connectionTimeout: 20 userParameter: low_signal: 8500 min_signal: 8000 @@ -84,6 +85,7 @@ rty: onFailure: buffer readOnly: false readoutPriority: on_request + connectionTimeout: 20 userParameter: tomo_additional_offsety: 0 rtz: @@ -98,6 +100,7 @@ rtz: onFailure: buffer readOnly: false readoutPriority: on_request + connectionTimeout: 20 # ############################################################ # ##################### OMNY samples ######################### @@ -165,6 +168,7 @@ ofzpx: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: -0.4317 ofzpy: @@ -184,6 +188,7 @@ ofzpy: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 0.7944 out: 0.6377 @@ -204,6 +209,7 @@ ofzpz: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 0 otransx: @@ -223,6 +229,7 @@ otransx: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 0 otransy: @@ -242,6 +249,7 @@ otransy: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: up_position: -1.2 gripper_sensorvoltagetarget: -2.30 @@ -262,6 +270,7 @@ otransz: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 0 osamx: @@ -281,6 +290,7 @@ osamx: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: -0.1 osamz: @@ -300,6 +310,7 @@ osamz: onFailure: buffer readOnly: true readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 0 oosay: @@ -319,6 +330,7 @@ oosay: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: near_field_in: 0.531 far_field_in: 0.4122 @@ -339,6 +351,7 @@ oosax: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: near_field_in: 3.2044 far_field_in: 3.022 @@ -359,6 +372,7 @@ oosaz: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: near_field_in: -0.4452 far_field_in: 6.5 @@ -379,6 +393,7 @@ oparkz: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 0 oshuttleopen: @@ -398,6 +413,7 @@ oshuttleopen: onFailure: buffer readOnly: true readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 0 oshuttlealign: @@ -417,6 +433,7 @@ oshuttlealign: onFailure: buffer readOnly: true readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 0 osamy: @@ -436,6 +453,7 @@ osamy: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 0 otracky: @@ -455,6 +473,7 @@ otracky: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: start_pos: -4.3431 osamroy: @@ -474,6 +493,7 @@ osamroy: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: in: 0 otrackz: @@ -493,6 +513,7 @@ otrackz: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: start_pos: -0.6948 oeyex: @@ -512,6 +533,7 @@ oeyex: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: xray_in: -45.7394 oeyez: @@ -531,6 +553,7 @@ oeyez: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: xray_in: -2 oeyey: @@ -550,6 +573,7 @@ oeyey: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: xray_in: 0.0229 @@ -572,6 +596,7 @@ ocsx: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: nothing: 0 ocsy: @@ -589,6 +614,7 @@ ocsy: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: nothing: 0 oshield: @@ -606,5 +632,6 @@ oshield: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 userParameter: nothing: 0 diff --git a/csaxs_bec/device_configs/user_template.yaml b/csaxs_bec/device_configs/user_template.yaml index 2cf6beb..689b53a 100644 --- a/csaxs_bec/device_configs/user_template.yaml +++ b/csaxs_bec/device_configs/user_template.yaml @@ -64,6 +64,7 @@ npx: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 deviceTags: - npoint npy: @@ -81,5 +82,6 @@ npy: onFailure: buffer readOnly: false readoutPriority: baseline + connectionTimeout: 20 deviceTags: - npoint \ No newline at end of file -- 2.49.1 From 8849b9ffea88d0d29d57b1bd0cffe6edd2bcd033 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 9 Feb 2026 10:10:44 +0100 Subject: [PATCH 04/12] fix(shutter); fix shutter_readback signal --- csaxs_bec/devices/epics/fast_shutter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/csaxs_bec/devices/epics/fast_shutter.py b/csaxs_bec/devices/epics/fast_shutter.py index 9064b15..0009550 100644 --- a/csaxs_bec/devices/epics/fast_shutter.py +++ b/csaxs_bec/devices/epics/fast_shutter.py @@ -5,7 +5,7 @@ that can be monitored to check the shutter status as it may be controlled direct the delay generator.""" from ophyd import Component as Cpt -from ophyd import Device, EpicsSignal, Kind +from ophyd import Device, EpicsSignal, EpicsSignalRO, Kind class cSAXSFastEpicsShutter(Device): @@ -19,7 +19,7 @@ class cSAXSFastEpicsShutter(Device): # PVs shutter = Cpt(EpicsSignal, "OUT_01", kind=Kind.normal, auto_monitor=True) - shutter_readback = Cpt(EpicsSignal, "IN_01", kind=Kind.normal, auto_monitor=True) + shutter_readback = Cpt(EpicsSignalRO, "IN_01", kind=Kind.normal, auto_monitor=True) # ----------------------------------------------------- # User-facing shutter control functions -- 2.49.1 From 32b4c39659f71d83f575d8977e5c4c17273b94f2 Mon Sep 17 00:00:00 2001 From: appel_c Date: Mon, 9 Feb 2026 14:11:00 +0100 Subject: [PATCH 05/12] refactor(ddg): Add fsh signal to ddg, improve trigger logic. --- .../epics/delay_generator_csaxs/ddg_1.py | 84 ++++++++++++++++--- .../delay_generator_csaxs.py | 1 + .../devices/epics/mcs_card/mcs_card_csaxs.py | 33 ++++++-- 3 files changed, 101 insertions(+), 17 deletions(-) diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py index 5adf4b1..a6eb98b 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py @@ -37,7 +37,9 @@ import traceback from typing import TYPE_CHECKING from bec_lib.logger import bec_logger -from ophyd_devices import CompareStatus, DeviceStatus, TransitionStatus +from ophyd import Component as Cpt +from ophyd import EpicsSignalRO, Kind +from ophyd_devices import CompareStatus, DeviceStatus, StatusBase, TransitionStatus from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import ( @@ -133,6 +135,24 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): USER_ACCESS = ["keep_shutter_open_during_scan", "set_trigger"] + fast_shutter_readback = Cpt( + EpicsSignalRO, + read_pv="X12SA-ES1-TTL:IN_01", + add_prefix=("",), # Add this to prevent the prefix to be added to the signal + kind=Kind.omitted, + auto_monitor=True, + ) + # The shutter control PV can indicate if the shutter is requested to be kept open. If that + # is the case, we can not use the signal shutter_readback signal to check if the delay cycle + # finishes but have to use the polling of the event status register to check if the burst finished. + fast_shutter_control = Cpt( + EpicsSignalRO, + read_pv="X12SA-ES1-TTL:OUT_01", + add_prefix=("",), # Add this to prevent the prefix to be added to the signal + kind=Kind.omitted, + auto_monitor=True, + ) + def __init__( self, name: str, @@ -195,7 +215,16 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): self.burst_delay.put(0) self.burst_count.put(1) - def keep_shutter_open_during_scan(self, open:True) -> None: + def keep_shutter_open_during_scan(self, open: True) -> None: + """ + Method to configure the delay generator for keeping the shutter open during a scans. + This means that the additional delay to open the shutter needs to be removed (2e-3) + from the timing of the signals. + + Args: + open (bool): If True, the shutter will be kept open during the scan. + If False, the shutter will be opened and closed for each trigger cycle. + """ if open is True: self._shutter_to_open_delay = 0 else: @@ -239,6 +268,18 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): if self.burst_count.get() != 1: self.burst_count.put(1) + ##################################### + ## Setup trigger source if needed ### + ##################################### + + # NOTE Some scans may change the trigger source to an external trigger, + # so we will make sure that the default trigger source is set for the DDG1 + # before each scan. If a scan requires a different trigger source, i.e. + # external triggers then the scan should implement this change after the + # on_stage method was called. + if self.trigger_source.get() != DEFAULT_TRIGGER_SOURCE: + self.set_trigger(DEFAULT_TRIGGER_SOURCE) + ######################################### ### Setup timing for burst and delays ### ######################################### @@ -249,7 +290,9 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # Burst Period DDG1 # Set burst_period to shutter width # c/t0 + self._shutter_to_open_delay + exp_time * burst_count - shutter_width = self._shutter_to_open_delay + exp_time * frames_per_trigger # Shutter starts closing at end of exposure + shutter_width = ( + self._shutter_to_open_delay + exp_time * frames_per_trigger + ) # Shutter starts closing at end of exposure if self.burst_period.get() != shutter_width: self.burst_period.put(shutter_width) @@ -443,7 +486,13 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): def on_trigger(self) -> DeviceStatus: """ - This method is called from BEC as a software trigger. + This method is called from BEC as a software trigger. Here the logic is as follows: + We check the signal of the "fast_shutter_control". If it is low, we know that the shutter + open/closes for each trigger cycle. In this case, we can use the signal of the + "fast_shutter_readback" to check if the shutter transitioned from open to close, which + indicates the end of the burst. If the "fast_shutter_control" signal is high, we know + that the shutter is kept open during the scan. In this case, we can only rely on polling + the event status register to check if the burst finished. It follows a specific procedure to ensure that the DDG1 and MCS card are properly handled on a trigger event. The established logic is as follows: @@ -464,9 +513,13 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): """ self._stop_polling() self._poll_thread_poll_loop_done.wait(timeout=1) + # TODO This may move to scan modifiers + if self.trigger_source.get() != TRIGGERSOURCE.SINGLE_SHOT.value: + status = StatusBase(obj=self) + status.set_finished() + return status # NOTE: This sleep is important to ensure that the HW is ready to process new commands. # It has been empirically determined after long testing that this improves stability. - time.sleep(0.02) # NOTE If the MCS card is present in the current session of BEC, # we prepare the card for the next trigger. The procedure is implemented @@ -482,12 +535,23 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # be investigated why the EPICS interface is slow to respond. status_mcs.wait(timeout=3) - # Prepare StatusBitsCompareStatus to resolve once the END_OF_BURST bit was set. - status = self._prepare_trigger_status_event() - - # Start polling thread again to monitor event status - self._start_polling() + if self.fast_shutter_control.get() == 0: + # Shutter is not kept open, we can rely on the shutter readback signal + status = TransitionStatus( + self.fast_shutter_readback, [1, 0] + ) # Wait for shutter to transition from open (1) to close (0) + else: + # NOTE This sleep is needed for 20ms to make sure that the HW of the DDG is + # again ready to process new commands. It was transferred from just after the + # _stop_polling() call, as it should only be relevant in case of polling the + # event status register, which may only be if the shutter is kept open. + time.sleep(0.02) + # Shutter is kept open, we can only rely on the event status register + status = self._prepare_trigger_status_event() + # Start polling thread again to monitor event status + self._start_polling() # Trigger the DDG1 + self.cancel_on_stop(status) self.trigger_shot.put(1, use_complete=True) return status diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/delay_generator_csaxs.py b/csaxs_bec/devices/epics/delay_generator_csaxs/delay_generator_csaxs.py index d0f1c1d..4789f5c 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/delay_generator_csaxs.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/delay_generator_csaxs.py @@ -488,6 +488,7 @@ class DelayGeneratorCSAXS(Device): name="trigger_source", kind=Kind.omitted, doc="Trigger Source for the DDG, options in TRIGGERSOURCE", + auto_monitor=True, ) trigger_level = Cpt( EpicsSignal, diff --git a/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py b/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py index f4a7b7c..9ceaf98 100644 --- a/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py +++ b/csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py @@ -22,7 +22,13 @@ import numpy as np from bec_lib.logger import bec_logger from ophyd import Component as Cpt from ophyd import EpicsSignalRO, Kind -from ophyd_devices import AsyncMultiSignal, CompareStatus, ProgressSignal, StatusBase, TransitionStatus +from ophyd_devices import ( + AsyncMultiSignal, + CompareStatus, + ProgressSignal, + StatusBase, + TransitionStatus, +) from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase from csaxs_bec.devices.epics.mcs_card.mcs_card import ( @@ -369,7 +375,6 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): elif self.scan_info.msg.scan_type == "fly": self.num_use_all.set(self._num_total_triggers).wait(timeout=self._pv_timeout) - # Clear any previous data, just to be sure with self._rlock: self._current_data.clear() @@ -392,10 +397,19 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): logger.info(f"MCS Card {self.name} on_stage completed in {time.time() - start_time:.3f}s.") # For a fly scan we need to start the mcs card ourselves if self.scan_info.msg.scan_type == "fly": - status_acquiring = TransitionStatus(self.acquiring, [ACQUIRING.DONE, ACQUIRING.ACQUIRING]) - self.cancel_on_stop(status_acquiring) self.erase_start.put(1) + def on_prescan(self) -> None | StatusBase: + """ + This method is called after on_stage and before the scan starts. For the MCS card, we need to make sure + that the card is properly started for fly scans. For step scans, this will be handled by the DDG, + so no action is required here. + """ + if self.scan_info.msg.scan_type == "fly": + status_acquiring = CompareStatus(self.acquiring, ACQUIRING.ACQUIRING) + self.cancel_on_stop(status_acquiring) + return status_acquiring + return None def on_unstage(self) -> None: """ @@ -439,7 +453,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): callback(exception=None) else: logger.info(f"Current data index is {self._current_data_index}") - if self._current_data_index >=1: + if self._current_data_index >= 1: for callback in self._scan_done_callbacks: callback(exception=None) @@ -470,7 +484,6 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): """Callback for status failure, the monitoring thread should be stopped.""" # NOTE Check for status.done and status.success is important to avoid if status.done: - self._start_monitor_async_data_emission.clear() # Stop monitoring def on_complete(self) -> CompareStatus: @@ -496,7 +509,13 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): monitoring thread is stopped properly. """ + # NOTE For fly scans with EXT/EN enabled triggering, the MCS card needs to receive an + # additional trigger at the end of the scan to advance the channel. This will ensure + # that the acquisition finishes on the card and that data is emitted to BEC. If the acquisition + # was already finished (i.e. normal step scan sends 1 extra pulse per burst cycle), this will + # not have any effect as the card will already be in DONE state and signal. self.software_channel_advance.put(1) + # Prepare and register status callback for the async monitoring loop status_async_data = StatusBase(obj=self) self._scan_done_callbacks.append(partial(self._status_callback, status_async_data)) @@ -510,7 +529,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard): # Combine both statuses ret_status = status & status_async_data - # Handle external stop/cancel, and stop monitoring + # NOTE: Handle external stop/cancel, and stop monitoring ret_status.add_callback(self._status_failed_callback) self.cancel_on_stop(ret_status) return ret_status -- 2.49.1 From 2814add2dee7fe1ea489b1f9118d1a8601a18061 Mon Sep 17 00:00:00 2001 From: x12sa Date: Tue, 10 Feb 2026 10:59:18 +0100 Subject: [PATCH 06/12] fixes at beamline --- csaxs_bec/device_configs/bl_endstation.yaml | 15 +++++++++++++++ csaxs_bec/device_configs/ptycho_flomni.yaml | 6 +++--- .../devices/epics/delay_generator_csaxs/ddg_1.py | 3 ++- csaxs_bec/devices/epics/fast_shutter.py | 2 +- csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py | 3 +++ csaxs_bec/devices/omny/shutter.py | 2 +- 6 files changed, 25 insertions(+), 6 deletions(-) diff --git a/csaxs_bec/device_configs/bl_endstation.yaml b/csaxs_bec/device_configs/bl_endstation.yaml index 3347e25..ffc291b 100644 --- a/csaxs_bec/device_configs/bl_endstation.yaml +++ b/csaxs_bec/device_configs/bl_endstation.yaml @@ -37,6 +37,21 @@ mcs: readoutPriority: monitored softwareTrigger: false + + +########################################################################## +########################### FAST SHUTTER ################################# +########################################################################## + +fsh: + description: Fast shutter manual control and readback + deviceClass: csaxs_bec.devices.epics.fast_shutter.cSAXSFastEpicsShutter + deviceConfig: + prefix: 'X12SA-ES1-TTL:' + onFailure: raise + enabled: true + readoutPriority: monitored + ########################################################################## ######################## SMARACT STAGES ################################## ########################################################################## diff --git a/csaxs_bec/device_configs/ptycho_flomni.yaml b/csaxs_bec/device_configs/ptycho_flomni.yaml index 7ca30d6..fd2bda1 100644 --- a/csaxs_bec/device_configs/ptycho_flomni.yaml +++ b/csaxs_bec/device_configs/ptycho_flomni.yaml @@ -158,7 +158,7 @@ fsamy: enabled: true onFailure: buffer readOnly: true - readoutPriority: baseline' + readoutPriority: baseline connectionTimeout: 20 userParameter: in: 2.75 @@ -460,8 +460,8 @@ flomni_temphum: # ########## OMNY / flOMNI / LamNI fast shutter ############## # ############################################################ omnyfsh: - description: omnyfsh connects to read fast shutter at X12 if in that network - deviceClass: csaxs_bec.devices.omny.shutter.OMNYFastEpicsShutter + description: omnyfsh connects to fast shutter at X12 if device fsh exists + deviceClass: csaxs_bec.devices.omny.shutter.OMNYFastShutter deviceConfig: {} enabled: true onFailure: buffer diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py index a6eb98b..6090454 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py @@ -137,7 +137,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): fast_shutter_readback = Cpt( EpicsSignalRO, - read_pv="X12SA-ES1-TTL:IN_01", + read_pv="X12SA-ES1-TTL:INP_01", add_prefix=("",), # Add this to prevent the prefix to be added to the signal kind=Kind.omitted, auto_monitor=True, @@ -513,6 +513,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): """ self._stop_polling() self._poll_thread_poll_loop_done.wait(timeout=1) + time.sleep(0.02) # TODO This may move to scan modifiers if self.trigger_source.get() != TRIGGERSOURCE.SINGLE_SHOT.value: status = StatusBase(obj=self) diff --git a/csaxs_bec/devices/epics/fast_shutter.py b/csaxs_bec/devices/epics/fast_shutter.py index 0009550..937f876 100644 --- a/csaxs_bec/devices/epics/fast_shutter.py +++ b/csaxs_bec/devices/epics/fast_shutter.py @@ -19,7 +19,7 @@ class cSAXSFastEpicsShutter(Device): # PVs shutter = Cpt(EpicsSignal, "OUT_01", kind=Kind.normal, auto_monitor=True) - shutter_readback = Cpt(EpicsSignalRO, "IN_01", kind=Kind.normal, auto_monitor=True) + shutter_readback = Cpt(EpicsSignalRO, "INP_01", kind=Kind.normal, auto_monitor=True) # ----------------------------------------------------- # User-facing shutter control functions diff --git a/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py b/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py index 92e3754..e0ee6f2 100644 --- a/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py +++ b/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py @@ -498,6 +498,9 @@ class RtFlomniController(Controller): ) # while scan is running while mode > 0: + + #TODO here?: scan abortion if no progress in scan *raise error + # logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}") mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status() time.sleep(0.01) diff --git a/csaxs_bec/devices/omny/shutter.py b/csaxs_bec/devices/omny/shutter.py index 983b632..c318ebf 100644 --- a/csaxs_bec/devices/omny/shutter.py +++ b/csaxs_bec/devices/omny/shutter.py @@ -13,7 +13,7 @@ from ophyd_devices import PSIDeviceBase logger = bec_logger.logger -class OMNYFastEpicsShutter(PSIDeviceBase, Device): +class OMNYFastShutter(PSIDeviceBase, Device): """ Fast Shutter control for OMNY setup. If started with at the beamline, it will expose the shutter control methods (fshopen, fshclose, fshstatus, fshinfo) from the -- 2.49.1 From a45aa094ef0bc87c340a05f414661d00a97371a7 Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 11 Feb 2026 08:52:38 +0100 Subject: [PATCH 07/12] refactor(ddg1): Use mcs acquiring status to resolve trigger of DDG1 --- .../epics/delay_generator_csaxs/ddg_1.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py index 6090454..994fd38 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py @@ -135,6 +135,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): USER_ACCESS = ["keep_shutter_open_during_scan", "set_trigger"] + # TODO Consider using the 'fsh' device instead. fast_shutter_readback = Cpt( EpicsSignalRO, read_pv="X12SA-ES1-TTL:INP_01", @@ -513,14 +514,14 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): """ self._stop_polling() self._poll_thread_poll_loop_done.wait(timeout=1) - time.sleep(0.02) - # TODO This may move to scan modifiers + + # NOTE If the trigger source is not SINGLE_SHOT, the DDG is triggered by an external source + # thus we can not expect that trigger signals are meant to be awaited for. In this case, + # we can directly return. if self.trigger_source.get() != TRIGGERSOURCE.SINGLE_SHOT.value: status = StatusBase(obj=self) status.set_finished() return status - # NOTE: This sleep is important to ensure that the HW is ready to process new commands. - # It has been empirically determined after long testing that this improves stability. # NOTE If the MCS card is present in the current session of BEC, # we prepare the card for the next trigger. The procedure is implemented @@ -536,11 +537,12 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # be investigated why the EPICS interface is slow to respond. status_mcs.wait(timeout=3) - if self.fast_shutter_control.get() == 0: - # Shutter is not kept open, we can rely on the shutter readback signal - status = TransitionStatus( - self.fast_shutter_readback, [1, 0] - ) # Wait for shutter to transition from open (1) to close (0) + # NOTE If the MCS card is present, we may also use its signal wait for the burst cycle to finish. + # This is True for any non-fly scans, as for fly scans the MCS card will not receive the additional + # trigger pulse from the ef delay pair, check on_stage. + if mcs is not None and mcs.enabled and self.scan_info.msg.scan_type != "fly": + # We can use the acquiring signal of the MCS card to check if the burst cycle is finished. + status = TransitionStatus(mcs.acquiring, [ACQUIRING.ACQUIRING, ACQUIRING.DONE]) else: # NOTE This sleep is needed for 20ms to make sure that the HW of the DDG is # again ready to process new commands. It was transferred from just after the -- 2.49.1 From 8195c12a35f1821771b63e585d9c904e502778a1 Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 11 Feb 2026 08:53:00 +0100 Subject: [PATCH 08/12] fix(fast-shutter): Add close shutter on 'stop' call. --- csaxs_bec/devices/epics/fast_shutter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/csaxs_bec/devices/epics/fast_shutter.py b/csaxs_bec/devices/epics/fast_shutter.py index 937f876..134d17c 100644 --- a/csaxs_bec/devices/epics/fast_shutter.py +++ b/csaxs_bec/devices/epics/fast_shutter.py @@ -51,6 +51,11 @@ class cSAXSFastEpicsShutter(Device): f"Fast shutter connected to EPICS channel: {pvname} with shutter readback: {shutter_readback_pvname}" ) + def stop(self, *, success: bool = False) -> None: + """Stop the shutter device. Make sure to close it.""" + self.shutter.put(0) + super().stop(success=success) + def help(self): """Display available user methods.""" print("Available methods:") -- 2.49.1 From f8d2af4c5b412db0205cdf096deca32a3a5703b6 Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 11 Feb 2026 09:45:40 +0100 Subject: [PATCH 09/12] fix(ddg1): revert to shutter signal as mcs status signal is too slow --- .../devices/epics/delay_generator_csaxs/ddg_1.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py index 994fd38..07a4d69 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py @@ -537,12 +537,15 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # be investigated why the EPICS interface is slow to respond. status_mcs.wait(timeout=3) - # NOTE If the MCS card is present, we may also use its signal wait for the burst cycle to finish. - # This is True for any non-fly scans, as for fly scans the MCS card will not receive the additional - # trigger pulse from the ef delay pair, check on_stage. - if mcs is not None and mcs.enabled and self.scan_info.msg.scan_type != "fly": - # We can use the acquiring signal of the MCS card to check if the burst cycle is finished. - status = TransitionStatus(mcs.acquiring, [ACQUIRING.ACQUIRING, ACQUIRING.DONE]) + # Use fast shutter readback in case it is not kept open + # NOTE: THe update frequency of the EpicsSIgnal is ~10Hz (100ms), needs to be checked if that can + # be adjusted to ~2kHz + if ( + self.fast_shutter_control.get() != 1 + ): # Shutter is not kept open, we can rely on its close signal. + status = TransitionStatus( + self.fast_shutter_readback, [1, 0] + ) # Add timeout error with explicit info update epics update freq else: # NOTE This sleep is needed for 20ms to make sure that the HW of the DDG is # again ready to process new commands. It was transferred from just after the -- 2.49.1 From 67ef20cfc8301660281f36635374de00de1de483 Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 11 Feb 2026 11:22:09 +0100 Subject: [PATCH 10/12] fix(ddg): fix logic to resolve trigger on ddg1. --- .../epics/delay_generator_csaxs/ddg_1.py | 67 +++++++++---------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py index 07a4d69..4c21169 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py @@ -488,12 +488,18 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): def on_trigger(self) -> DeviceStatus: """ This method is called from BEC as a software trigger. Here the logic is as follows: - We check the signal of the "fast_shutter_control". If it is low, we know that the shutter - open/closes for each trigger cycle. In this case, we can use the signal of the - "fast_shutter_readback" to check if the shutter transitioned from open to close, which - indicates the end of the burst. If the "fast_shutter_control" signal is high, we know - that the shutter is kept open during the scan. In this case, we can only rely on polling - the event status register to check if the burst finished. + + We first check if the trigger_source is set to SINGLE_SHOT. Only then will we received, + otherwise we return a status object directly as the DDG is triggered by an external + source which will have to implement its own logic to wait for trigger signals to + be received. + + I SINGLE_SHOT, the implementation here will send a software trigger. Now there are + two options to wait for the trigger (burst) cycle to be done. One is to rely on the + signal of the "mcs" card if it is present. However, this is only possible if the + scan_type is not "fly" as in fly scans the ef channel is not triggered to send the last + pulse to the card (but the card is finishing its acquisition in complete itself). Then + we rely on the polling of the event status register to check if the burst cycle is done. It follows a specific procedure to ensure that the DDG1 and MCS card are properly handled on a trigger event. The established logic is as follows: @@ -512,8 +518,8 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): - Return the status object to BEC which will automatically resolve once the status register has the END_OF_BURST bit set. The callback of the status object will also stop the polling loop. """ + overall_start = time.time() self._stop_polling() - self._poll_thread_poll_loop_done.wait(timeout=1) # NOTE If the trigger source is not SINGLE_SHOT, the DDG is triggered by an external source # thus we can not expect that trigger signals are meant to be awaited for. In this case, @@ -525,40 +531,33 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # NOTE If the MCS card is present in the current session of BEC, # we prepare the card for the next trigger. The procedure is implemented - # in the '_prepare_mcs_on_trigger' method. - # Prepare the MCS card for the next software trigger + # in the '_prepare_mcs_on_trigger' method. We will also use the mcs card + # to indicate when the burst cycle is done. If no mcs card is available + # the fallback is to use the polling of the DDG mcs = self.device_manager.devices.get("mcs", None) - if mcs is None or mcs.enabled is False: - logger.info("Did not find mcs card with name 'mcs' in current session") - else: - status_mcs = self._prepare_mcs_on_trigger(mcs) - # NOTE Timeout of 3s should be plenty, any longer wait should checked. If this happens to crash - # an acquisition regularly with a WaitTimeoutError, the timeout can be increased but it should - # be investigated why the EPICS interface is slow to respond. - status_mcs.wait(timeout=3) - - # Use fast shutter readback in case it is not kept open - # NOTE: THe update frequency of the EpicsSIgnal is ~10Hz (100ms), needs to be checked if that can - # be adjusted to ~2kHz - if ( - self.fast_shutter_control.get() != 1 - ): # Shutter is not kept open, we can rely on its close signal. - status = TransitionStatus( - self.fast_shutter_readback, [1, 0] - ) # Add timeout error with explicit info update epics update freq - else: - # NOTE This sleep is needed for 20ms to make sure that the HW of the DDG is - # again ready to process new commands. It was transferred from just after the - # _stop_polling() call, as it should only be relevant in case of polling the - # event status register, which may only be if the shutter is kept open. + if mcs is None or mcs.enabled is False or self.scan_info.msg.scan_type == "fly": + self._poll_thread_poll_loop_done.wait(timeout=1) + logger.warning("Did not find mcs card with name 'mcs' in current session") time.sleep(0.02) # Shutter is kept open, we can only rely on the event status register status = self._prepare_trigger_status_event() # Start polling thread again to monitor event status self._start_polling() - # Trigger the DDG1 - self.cancel_on_stop(status) + else: + start_time = time.time() + logger.debug(f"Preparing mcs card ") + status_mcs = self._prepare_mcs_on_trigger(mcs) + # NOTE Timeout of 3s should be plenty, any longer wait should checked. If this happens to crash + # an acquisition regularly with a WaitTimeoutError, the timeout can be increased but it should + # be investigated why the EPICS interface is slow to respond. + status_mcs.wait(timeout=3) + status = TransitionStatus(mcs.acquiring, [ACQUIRING.ACQUIRING, ACQUIRING.DONE]) + logger.debug(f"Finished preparing mcs card {time.time()-start_time}") + + # Send trigger self.trigger_shot.put(1, use_complete=True) + self.cancel_on_stop(status) + logger.info(f"Configured ddg in {time.time()-overall_start}") return status def on_stop(self) -> None: -- 2.49.1 From 75cc672f0870418d5ce1f8315022fae45064f8d1 Mon Sep 17 00:00:00 2001 From: appel_c Date: Wed, 11 Feb 2026 11:50:22 +0100 Subject: [PATCH 11/12] tests: fix tests for ddg1 --- .../test_delay_generator_csaxs.py | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/tests_devices/test_delay_generator_csaxs.py b/tests/tests_devices/test_delay_generator_csaxs.py index 18079c6..6ba4ecb 100644 --- a/tests/tests_devices/test_delay_generator_csaxs.py +++ b/tests/tests_devices/test_delay_generator_csaxs.py @@ -282,10 +282,11 @@ def test_ddg1_stage(mock_ddg1: DDG1): mock_ddg1.scan_info.msg.scan_parameters["exp_time"] = exp_time mock_ddg1.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger + mock_ddg1.fast_shutter_control._read_pv.mock_data = 0 # Simulate shutter control mock_ddg1.stage() - shutter_width = 2e-3 + exp_time * frames_per_trigger + 1e-3 + shutter_width = mock_ddg1._shutter_to_open_delay + exp_time * frames_per_trigger assert np.isclose(mock_ddg1.burst_mode.get(), 1) # burst mode is enabled assert np.isclose(mock_ddg1.burst_delay.get(), 0) @@ -302,6 +303,25 @@ def test_ddg1_stage(mock_ddg1: DDG1): assert np.isclose(mock_ddg1.ef.width.get(), 1e-6) assert mock_ddg1.staged == ophyd.Staged.yes + mock_ddg1.unstage() + + # Test if shutter is kept open.. + mock_ddg1.fast_shutter_control._read_pv.mock_data = 1 # Simulate shutter control is kept open + # Test method + mock_ddg1.keep_shutter_open_during_scan(True) + shutter_width = mock_ddg1._shutter_to_open_delay + exp_time * frames_per_trigger + assert np.isclose( + shutter_width, exp_time * frames_per_trigger + ) # Shutter to open delay is not added as shutter is kept open + # Simulate fly scan, so no extra trigger for MCS card. + mock_ddg1.scan_info.msg.scan_type = "fly" + mock_ddg1.stage() + # Shutter channel cd + assert np.isclose(mock_ddg1.cd.delay.get(), 0) + assert np.isclose(mock_ddg1.cd.width.get(), shutter_width) + # MCS channel ef or gate + assert np.isclose(mock_ddg1.ef.delay.get(), 0) + assert np.isclose(mock_ddg1.ef.width.get(), 0) # No triggering of MCS due to shutter fly scan def test_ddg1_on_trigger(mock_ddg1: DDG1): @@ -331,9 +351,28 @@ def test_ddg1_on_trigger(mock_ddg1: DDG1): ################################# with mock.patch.object(ddg, "_prepare_mcs_on_trigger") as mock_prepare_mcs: mock_prepare_mcs.return_value = ophyd.StatusBase(done=True, success=True) + # MCS card is present and enabled, should call prepare_mcs_on_trigger + # and the status should resolve once acuiring goes from 1 to 0. status = ddg.trigger() + assert status.done is False + mcs = ddg.device_manager.devices.get("mcs", None) + assert mcs is not None + mcs.acquiring._read_pv.mock_data = 1 # Simulate acquiring started + assert status.done is False + mcs.acquiring._read_pv.mock_data = 0 # Simulate acquiring stopped + status.wait(timeout=1) # Wait for the status to be done + assert status.done is True + assert status.success is True + mock_prepare_mcs.assert_called_once() + # Now we disable the mcs card, and trigger again. This should not call prepare_mcs_on_trigger + # and should fallback to polling the DDG for END_OF_BURST status bit. + + # Disable mcs card + mcs.enabled = False + status = ddg.trigger() # Check that the poll thread run event is set + # Careful in debugger, there is a timeout based on the exp_time + 5s default assert ddg._poll_thread_run_event.is_set() assert not ddg._poll_thread_poll_loop_done.is_set() -- 2.49.1 From 7a2c6629f77e8bfb1fc95a979ef0f83e9aa46848 Mon Sep 17 00:00:00 2001 From: x12sa Date: Mon, 16 Feb 2026 10:18:37 +0100 Subject: [PATCH 12/12] refactor: add gh signal for ddg1 with TTL for full acquisition --- csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py index 4c21169..79e1bcd 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py @@ -309,6 +309,8 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): # Shutter opens without delay at t0, closes after exp_time * burst_count + 2ms (self._shutter_to_open_delay) self.set_delay_pairs(channel="cd", delay=0, width=shutter_width) + self.set_delay_pairs(channel="gh", delay=self._shutter_to_open_delay, width=(shutter_width-self._shutter_to_open_delay)) + # Trigger extra pulse for MCS OR gate # f = e + 1us # e has refernce to d, f has reference to e -- 2.49.1