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()