diff --git a/csaxs_bec/device_configs/bec_device_config_sastt.yaml b/csaxs_bec/device_configs/bec_device_config_sastt.yaml index 142794f..35dc259 100644 --- a/csaxs_bec/device_configs/bec_device_config_sastt.yaml +++ b/csaxs_bec/device_configs/bec_device_config_sastt.yaml @@ -53,89 +53,89 @@ eiger9m: enabled: true readoutPriority: async softwareTrigger: false -ddg_detectors: - description: DelayGenerator for detector triggering - deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS - deviceConfig: - prefix: 'delaygen:DG1:' - ddg_config: - delay_burst: 40.e-3 - delta_width: 0 - additional_triggers: 0 - polarity: - - 1 # T0 -> DDG MCS - - 0 # eiger - - 1 # falcon - - 1 - - 1 - amplitude: 4.5 - offset: 0 - thres_trig_level: 2.5 - set_high_on_exposure: False - set_high_on_stage: False - deviceTags: - - cSAXS - - ddg_detectors - onFailure: buffer - enabled: true - readoutPriority: async - softwareTrigger: false -ddg_mcs: - description: DelayGenerator for mcs triggering - deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS - deviceConfig: - prefix: 'delaygen:DG2:' - ddg_config: - delay_burst: 0 - delta_width: 0 - additional_triggers: 1 - polarity: - - 1 - - 0 - - 1 - - 1 - - 1 - amplitude: 4.5 - offset: 0 - thres_trig_level: 2.5 - set_high_on_exposure: False - set_high_on_stage: False - set_trigger_source: EXT_RISING_EDGE - trigger_width: 3.e-3 - deviceTags: - - cSAXS - - ddg_mcs - onFailure: buffer - enabled: true - readoutPriority: async - softwareTrigger: false -ddg_fsh: - description: DelayGenerator for fast shutter control - deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS - deviceConfig: - prefix: 'delaygen:DG3:' - ddg_config: - delay_burst: 0 - delta_width: 80.e-3 - additional_triggers: 0 - polarity: - - 1 - - 1 - - 1 - - 1 - - 1 - amplitude: 4.5 - offset: 0 - thres_trig_level: 2.5 - set_high_on_exposure: True - set_high_on_stage: False - deviceTags: - - cSAXS - - ddg_fsh - onFailure: buffer - enabled: true - readoutPriority: async - softwareTrigger: false +# ddg_detectors: +# description: DelayGenerator for detector triggering +# deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS +# deviceConfig: +# prefix: 'delaygen:DG1:' +# ddg_config: +# delay_burst: 40.e-3 +# delta_width: 0 +# additional_triggers: 0 +# polarity: +# - 1 # T0 -> DDG MCS +# - 0 # eiger +# - 1 # falcon +# - 1 +# - 1 +# amplitude: 4.5 +# offset: 0 +# thres_trig_level: 2.5 +# set_high_on_exposure: False +# set_high_on_stage: False +# deviceTags: +# - cSAXS +# - ddg_detectors +# onFailure: buffer +# enabled: true +# readoutPriority: async +# softwareTrigger: false +# ddg_mcs: +# description: DelayGenerator for mcs triggering +# deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS +# deviceConfig: +# prefix: 'delaygen:DG2:' +# ddg_config: +# delay_burst: 0 +# delta_width: 0 +# additional_triggers: 1 +# polarity: +# - 1 +# - 0 +# - 1 +# - 1 +# - 1 +# amplitude: 4.5 +# offset: 0 +# thres_trig_level: 2.5 +# set_high_on_exposure: False +# set_high_on_stage: False +# set_trigger_source: EXT_RISING_EDGE +# trigger_width: 3.e-3 +# deviceTags: +# - cSAXS +# - ddg_mcs +# onFailure: buffer +# enabled: true +# readoutPriority: async +# softwareTrigger: false +# ddg_fsh: +# description: DelayGenerator for fast shutter control +# deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS +# deviceConfig: +# prefix: 'delaygen:DG3:' +# ddg_config: +# delay_burst: 0 +# delta_width: 80.e-3 +# additional_triggers: 0 +# polarity: +# - 1 +# - 1 +# - 1 +# - 1 +# - 1 +# amplitude: 4.5 +# offset: 0 +# thres_trig_level: 2.5 +# set_high_on_exposure: True +# set_high_on_stage: False +# deviceTags: +# - cSAXS +# - ddg_fsh +# onFailure: buffer +# enabled: true +# readoutPriority: async +# softwareTrigger: false falcon: description: Falcon detector x-ray fluoresence deviceClass: csaxs_bec.devices.epics.falcon_csaxs.FalconcSAXS diff --git a/csaxs_bec/device_configs/ddg_test.yaml b/csaxs_bec/device_configs/ddg_test.yaml new file mode 100644 index 0000000..e79b771 --- /dev/null +++ b/csaxs_bec/device_configs/ddg_test.yaml @@ -0,0 +1,34 @@ +ddg_master: + description: Main delay Generator for triggering + deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DDGMaster + enabled: true + deviceConfig: + prefix: 'X12SA-CPCL-DDG1:' + onFailure: raise + readOnly: false + readoutPriority: baseline + softwareTrigger: true + +samx: + readoutPriority: baseline + deviceClass: ophyd_devices.SimPositioner + deviceConfig: + delay: 1 + limits: + - -50 + - 50 + tolerance: 0.01 + update_frequency: 400 + deviceTags: + - user motors + enabled: true + readOnly: false + +bpm4i: + readoutPriority: monitored + deviceClass: ophyd_devices.SimMonitor + deviceConfig: + deviceTags: + - beamline + enabled: true + readOnly: false \ No newline at end of file diff --git a/csaxs_bec/device_configs/epics_devices_config.yaml b/csaxs_bec/device_configs/epics_devices_config.yaml index 546f085..1f69623 100644 --- a/csaxs_bec/device_configs/epics_devices_config.yaml +++ b/csaxs_bec/device_configs/epics_devices_config.yaml @@ -1,30 +1,30 @@ -ddg_detectors: - description: DelayGenerator for detector triggering - deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS - deviceConfig: - prefix: 'X12SA-CPCL-DDG3:' - ddg_config: - delay_burst: 40.e-3 - delta_width: 0 - additional_triggers: 0 - polarity: - - 1 # T0 -> DDG MCS - - 0 # eiger - - 1 # falcon - - 1 - - 1 - amplitude: 4.5 - offset: 0 - thres_trig_level: 2.5 - set_high_on_exposure: False - set_high_on_stage: False - deviceTags: - - cSAXS - - ddg_detectors - onFailure: buffer - enabled: true - readoutPriority: async - softwareTrigger: True +# ddg_detectors: +# description: DelayGenerator for detector triggering +# deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS +# deviceConfig: +# prefix: 'X12SA-CPCL-DDG3:' +# ddg_config: +# delay_burst: 40.e-3 +# delta_width: 0 +# additional_triggers: 0 +# polarity: +# - 1 # T0 -> DDG MCS +# - 0 # eiger +# - 1 # falcon +# - 1 +# - 1 +# amplitude: 4.5 +# offset: 0 +# thres_trig_level: 2.5 +# set_high_on_exposure: False +# set_high_on_stage: False +# deviceTags: +# - cSAXS +# - ddg_detectors +# onFailure: buffer +# enabled: true +# readoutPriority: async +# softwareTrigger: True bpm4i: readoutPriority: monitored deviceClass: ophyd_devices.SimMonitor diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/__init__.py b/csaxs_bec/devices/epics/delay_generator_csaxs/__init__.py index 8de5d58..0b883c8 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/__init__.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/__init__.py @@ -1 +1 @@ -from .ddg_master import DDGMaster +from .ddg_1 import DDG1 diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py new file mode 100644 index 0000000..eea5bcf --- /dev/null +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py @@ -0,0 +1,145 @@ +import atexit +import time +from threading import Event, Thread + +from bec_lib.logger import bec_logger +from ophyd import DeviceStatus, StatusBase +from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase + +from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import ( + CHANNELREFERENCE, + OUTPUTPOLARITY, + STATUSBITS, + TRIGGERSOURCE, + AllChannelNames, + ChannelConfig, + DelayGeneratorCSAXS, + StatusBitsCompareStatus, +) +from csaxs_bec.devices.epics.delay_generator_csaxs.error_registry import ERROR_CODES + +logger = bec_logger.logger + +_DEFAULT_CHANNEL_CONFIG: ChannelConfig = { + "amplitude": 5.0, + "offset": 0.0, + "polarity": OUTPUTPOLARITY.POSITIVE, + "mode": "ttl", +} + +DEFAULT_IO_CONFIG: dict[AllChannelNames, ChannelConfig] = { + "t0": _DEFAULT_CHANNEL_CONFIG, + "ab": _DEFAULT_CHANNEL_CONFIG, + "cd": _DEFAULT_CHANNEL_CONFIG, + "ef": _DEFAULT_CHANNEL_CONFIG, + "gh": _DEFAULT_CHANNEL_CONFIG, +} +DEFAULT_TRIGGER_SOURCE: TRIGGERSOURCE = TRIGGERSOURCE.SINGLE_SHOT +DEFAULT_DEAD_TIMES_S = {"ab": 1e-3, "cd": 1e-3, "ef": 1e-3, "gh": 1e-3} + + +class DDG1(PSIDeviceBase, DelayGeneratorCSAXS): + """ + Implementation of DelayGeneratorCSAXS for the CSAXS master trigger delay generator at X12SA-CPCL-DDG1 + """ + + # pylint: disable=attribute-defined-outside-init + def on_connected(self) -> None: + """Set the default values on the device - intended to overwrite everything to a usable default state. + Sets DEFAULT_IO_CONFIG into each channel, + sets the trigger source to DEFAULT_TRIGGER_SOURCE, + and turns off burst mode""" + self.burst_disable() # it is possible to miss setting settings if burst is enabled + for channel, config in DEFAULT_IO_CONFIG.items(): + self.set_io_values(channel, **config) + self.set_trigger(DEFAULT_TRIGGER_SOURCE) + self.set_references_for_channels( + [ + ("A", CHANNELREFERENCE.T0), + ("B", CHANNELREFERENCE.A), + ("C", CHANNELREFERENCE.T0), + ("D", CHANNELREFERENCE.C), + ("E", CHANNELREFERENCE.D), # One extra pulse once shutter closes for MCS + ("F", CHANNELREFERENCE.E), + ("G", CHANNELREFERENCE.T0), + ("H", CHANNELREFERENCE.G), + ] + ) + # Start background thread to poll status register + # self._status_polling_stop_event = Event() + # self._status_polling_thread = Thread(target=self._poll_status) + # self._status_polling_thread.start() + # atexit.register(self.on_destroy) + + # def _poll_status(self): + # """The status register has to be actively pulled, triggering the proc_status results in + # event_status being updated, which in turn allows the StatusBitsCompareStatus from on_trigger + # to be updated and eventually resolve.""" + # dispatcher = self.state.proc_status.cl.get_dispatcher() + # event = dispatcher.stop_event + # while not (self._status_polling_stop_event.is_set() or event.is_set()): + # try: + # # Call with timeout to avoid blocking in shutdown + # self.state.proc_status.put(1, timeout=1) + # except TimeoutError: + # # If any of the stop events are set, stop polling + # if self._status_polling_stop_event.is_set() or event.is_set(): + # logger.info("Exiting _poll_status thread loop for DDG.") + # break + # time.sleep(1 / 5) # poll the status at 5 Hz + + def on_stage(self) -> DeviceStatus | StatusBase | None: + exp_time = self.scan_info.msg.scan_parameters["exp_time"] + frames_per_trigger = self.scan_info.msg.scan_parameters["frames_per_trigger"] + readout_time = self.scan_info.msg.scan_parameters["readout_time"] + + if readout_time is not None and readout_time != 0: + # If we are given a single readout time from BEC, use it for all 4 channels + pulse_widths = [exp_time - readout_time] * len(DEFAULT_DEAD_TIMES_S) + else: + # Otherwise, derive the pulse widths from the default dead times defined above + pulse_widths = [exp_time - DEFAULT_DEAD_TIMES_S[ch] for ch in DEFAULT_DEAD_TIMES_S] + logger.info(f"setting pulse widths to {pulse_widths}") + self.set_delay_pairs(["ab", "cd", "ef", "gh"], delay=0, width=pulse_widths) + self.burst_enable(count=frames_per_trigger, delay=0, period=exp_time) + + def on_trigger(self) -> DeviceStatus | StatusBase | None: + """ Note, we need to add a delay to the StatusBits callback on the event_status. + If we don't then subsequent triggers may reach the DDG too early, and will be ignored. To + avoid this, we've added the option to specify a delay via add_delay, default here is 50ms.""" + st = StatusBase()#StatusBitsCompareStatus(self.state.event_status, STATUSBITS.END_OF_BURST, run=False) + self.cancel_on_stop(st) + self.trigger_shot.put(1, use_complete=True) + time.sleep(self.scan_info.msg.scan_parameters["exp_time"]) + timer = 0 + # TODO make asynchronous and nicer! + while st.done is False: + self.state.proc_status.put(1,use_complete=True) + # Do I need to give this time to update? ask Xiaoqiang!! + event_status = self.state.event_status.get() + if (STATUSBITS(event_status) & STATUSBITS.END_OF_BURST) == STATUSBITS.END_OF_BURST: + st.set_finished() + timer += 0.1 + time.sleep(0.1) + if timer > 1: + st.set_exception(TimeoutError(f"Device {self.name} failed to finish trigger")) + break + time.sleep(0.05) + return st + + def on_destroy(self) -> None: + return + if getattr(self, "_status_polling_stop_event", None) is not None: + self._status_polling_stop_event.set() + if getattr(self, "_status_polling_thread", None) is not None: + self._status_polling_thread.join(timeout=3) + + def on_stop(self) -> None: + """Stop the delay generator by setting the burst mode to 0""" + self.stop_ddg() + + +if __name__ == "__main__": + ddg = DDG1(name="ddg", prefix="X12SA-CPCL-DDG1:") + ddg.wait_for_connection(all_signals=True, timeout=30) + ddg.summary() diff --git a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_master.py b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py similarity index 71% rename from csaxs_bec/devices/epics/delay_generator_csaxs/ddg_master.py rename to csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py index 305af73..95b27b1 100644 --- a/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_master.py +++ b/csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py @@ -38,7 +38,7 @@ DEFAULT_TRIGGER_SOURCE: TRIGGERSOURCE = TRIGGERSOURCE.SINGLE_SHOT DEFAULT_DEAD_TIMES_S = {"ab": 1e-3, "cd": 1e-3, "ef": 1e-3, "gh": 1e-3} -class DDGMaster(PSIDeviceBase, DelayGeneratorCSAXS): +class DDG2(PSIDeviceBase, DelayGeneratorCSAXS): """ Implementation of DelayGeneratorCSAXS for the CSAXS master trigger delay generator at X12SA-CPCL-DDG1 """ @@ -59,34 +59,12 @@ class DDGMaster(PSIDeviceBase, DelayGeneratorCSAXS): ("B", CHANNELREFERENCE.A), ("C", CHANNELREFERENCE.T0), ("D", CHANNELREFERENCE.C), - ("E", CHANNELREFERENCE.T0), + ("E", CHANNELREFERENCE.T0),# One extra pulse once shutter closes for MCS ("F", CHANNELREFERENCE.E), ("G", CHANNELREFERENCE.T0), ("H", CHANNELREFERENCE.G), ] ) - # Start background thread to poll status register - self._status_polling_stop_event = Event() - self._status_polling_thread = Thread(target=self._poll_status) - self._status_polling_thread.start() - atexit.register(self.on_destroy) - - def _poll_status(self): - """The status register has to be actively pulled, triggering the proc_status results in - event_status being updated, which in turn allows the StatusBitsCompareStatus from on_trigger - to be updated and eventually resolve.""" - dispatcher = self.state.proc_status.cl.get_dispatcher() - event = dispatcher.stop_event - while not (self._status_polling_stop_event.is_set() or event.is_set()): - try: - # Call with timeout to avoid blocking in shutdown - self.state.proc_status.put(1, timeout=1) - except TimeoutError: - # If any of the stop events are set, stop polling - if self._status_polling_stop_event.is_set() or event.is_set(): - logger.info("Exiting _poll_status thread loop for DDG.") - break - time.sleep(1 / 5) # poll the status at 5 Hz def on_stage(self) -> DeviceStatus | StatusBase | None: exp_time = self.scan_info.msg.scan_parameters["exp_time"] @@ -104,12 +82,31 @@ class DDGMaster(PSIDeviceBase, DelayGeneratorCSAXS): self.burst_enable(count=frames_per_trigger, delay=0, period=exp_time) def on_trigger(self) -> DeviceStatus | StatusBase | None: - st = StatusBitsCompareStatus(self.state.event_status, STATUSBITS.END_OF_BURST, run=False) + """ Note, we need to add a delay to the StatusBits callback on the event_status. + If we don't then subsequent triggers may reach the DDG too early, and will be ignored. To + avoid this, we've added the option to specify a delay via add_delay, default here is 50ms.""" + st = StatusBase()#StatusBitsCompareStatus(self.state.event_status, STATUSBITS.END_OF_BURST, run=False) self.cancel_on_stop(st) - self.trigger_shot.put(1) + self.trigger_shot.put(1, use_complete=True) + time.sleep(self.scan_info.msg.scan_parameters["exp_time"]) + timer = 0 + # TODO make asynchronous and nicer! + while st.done is False: + self.state.proc_status.put(1,use_complete=True) + # Do I need to give this time to update? ask Xiaoqiang!! + event_status = self.state.event_status.get() + if (STATUSBITS(event_status) & STATUSBITS.END_OF_BURST) == STATUSBITS.END_OF_BURST: + st.set_finished() + timer += 0.1 + time.sleep(0.1) + if timer > 1: + st.set_exception(TimeoutError(f"Device {self.name} failed to finish trigger")) + break + time.sleep(0.05) return st def on_destroy(self) -> None: + return if getattr(self, "_status_polling_stop_event", None) is not None: self._status_polling_stop_event.set() if getattr(self, "_status_polling_thread", None) is not None: @@ -121,6 +118,6 @@ class DDGMaster(PSIDeviceBase, DelayGeneratorCSAXS): if __name__ == "__main__": - ddg = DDGMaster(name="ddg", prefix="X12SA-CPCL-DDG1:") + ddg = DDG1(name="ddg", prefix="X12SA-CPCL-DDG1:") ddg.wait_for_connection(all_signals=True, timeout=30) ddg.summary() 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 68a939b..a04b9c9 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 @@ -7,6 +7,7 @@ https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf import enum from typing import Literal, TypedDict +import time from bec_lib.logger import bec_logger from ophyd import Component as Cpt @@ -113,6 +114,7 @@ class StatusBitsCompareStatus(SubscriptionStatus): *args, event_type=None, timeout: float | None = None, + add_delay:float|None = None, settle_time: float = 0, run: bool = True, **kwargs, @@ -120,6 +122,7 @@ class StatusBitsCompareStatus(SubscriptionStatus): """Initialize the compare status with a signal.""" self._signal = signal self._value = value + self._add_delay = add_delay or 0 self._raise_states = raise_states or [] super().__init__( device=signal, @@ -132,6 +135,12 @@ class StatusBitsCompareStatus(SubscriptionStatus): def _compare_callback(self, value, **kwargs) -> bool: """Callback for subscription status""" + obj = kwargs.get("obj", None) + if obj is None: + name = 'no object received' + else: + name=obj.name + logger.info(f"Receive update for {name} with value {value}") if any((STATUSBITS(value) & state) == state for state in self._raise_states): self.set_exception( ValueError( @@ -139,6 +148,10 @@ class StatusBitsCompareStatus(SubscriptionStatus): ) ) return False + if self._add_delay !=0: + logger.info(f"Sleeping for {self._add_delay} for {name}") + time.sleep(self._add_delay) + return (STATUSBITS(value) & self._value) == self._value